chore: bump version to 2.1.36 - #3610
Merged
Merged
Conversation
piorpua
enabled auto-merge (squash)
July 17, 2026 12:25
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
acetaxxxx
added a commit
to acetaxxxx/AionUi
that referenced
this pull request
Aug 27, 2026
* fix: prepare backend startup directories (#3536)
Co-authored-by: zynx <>
* fix(installer): harden Windows failure reporting and self-lock handling (#3533)
* Fix installer failure reporting coverage
* Fix Windows installer self-lock diagnostics
* style(installer): format self-lock smoke test
---------
Co-authored-by: aionwin <win@aionui.com>
* fix(guid): adapt assistant pills to narrow widths (#3537)
Co-authored-by: zk <zk@users.noreply.local>
* chore: bump version to 2.1.31 and aioncore to v0.1.44 (#3538)
Co-authored-by: zynx <>
* feat(conversation): rework message queue into a send draft box (#3547)
* feat(conversation): add auto/manual send mode and send-now to command queue
Extend the queue state with a per-conversation send mode (auto | manual,
defaults to auto) and two new actions:
- sendNow(commandId): dequeue and execute a single queued command
immediately, leaving the rest of the queue (mode, order, paused flag)
untouched.
- toggleMode(): switch between auto (drain automatically after each turn)
and manual (hold queued messages until sent one by one).
Manual mode gates the auto-drain effect so nothing sends on its own. The
mode is persisted alongside the queue in session storage and preserved
across all state updates.
* feat(conversation): redesign command queue as send draft box
Rework the queue panel into a 'draft box' with clearer, always-visible
controls:
- Each queued message now shows three inline actions: send now, edit and
remove (edit was previously hidden in the overflow menu).
- Send now interrupts the current reply, then sends that message; wired in
both the ACP and aionrs send boxes via handleSendNowQueued.
- Header row shows the draft box title with a count badge, an auto/manual
mode toggle, a help tooltip explaining both modes, and an overflow menu.
- Clear moved into the overflow menu with a danger-styled confirm dialog.
- Narrow / mobile layout: title collapses to an icon, help folds into the
overflow menu, and rows are reordered by long-pressing the whole row
instead of a hover-only handle.
* chore(i18n): add send draft box strings
Rename the command queue title to 'draft box', rename clear to 'clear
draft box', and add keys for the send-now action, auto/manual mode labels,
mode toggle, help text and the clear confirm dialog across all 12 locales.
Regenerate i18n key types.
* test(conversation): cover send draft box mode and send-now
Update the panel test for the new three-action layout, mode toggle and
confirm-based clear. Add hook tests covering the default auto mode, toggle,
manual mode not auto-draining, resuming auto drain, sendNow targeting a
single command, and manual mode restored from storage. Include the PRD.
* chore: ignore local .analysis scratch directory
* fix(i18n): update Russian localization (#3541)
Co-authored-by: Dr.LMA <lma@mbp.lan>
* feat(conversation): rework model selector into a two-level menu (#3550)
* feat(agent): add two-level runtime selector building blocks
Shared pieces for the model selector's two-level dropdown, reused by every
model selector (ACP, aionrs, Guid), so the dropdown look & behaviour lives in
one place:
- RuntimeSelectorModelList: model options with optional provider grouping and
a search box shown once the model count exceeds MODEL_SEARCH_THRESHOLD (5),
filtering across groups. It owns a single scroll container with the search
box fixed above it (outside the scroll area) so the search box never moves or
leaves a gap; grouped provider titles stick to the top while scrolling.
- RuntimeSelectorSubMenuTitle: a first-level row (label + current value); no
chevron of its own since Menu.SubMenu draws the expand arrow.
- RUNTIME_SUBMENU_TRIGGER_PROPS: opens submenus to the left with auto-flip.
- AionInlineSearchInput: lightweight dropdown search box mirroring the one from
#3532 so it can be swapped for the shared component once that merges.
CSS: disable the Arco popup's own scroll/height cap wherever it hosts the list
(keyed off :has(.runtime-model-scroll)) so there is exactly one scrollbar, and
pin provider group titles to the top of that scroll container.
Drop the now-unused renderThoughtLevelMenuGroup and RuntimeSelectorMenuDivider.
* feat(conversation): two-level model selector across all surfaces
Rework the model dropdown from one long menu (thought-level group + model
group stacked) into a two-level menu: a first level with just Model and
Reasoning-effort rows, each expanding into a left-side submenu with the full
option list. The first level stays short no matter how many models there are.
- AcpModelSelector (Claude/Codex/Gemini CLI, single + group chat): two-level
when a thought level exists; otherwise the dropdown is the model list itself.
- AionrsModelSelector (Aion CLI conversation): same two-level structure, model
submenu keeps provider grouping.
- GuidModelSelector (home): ACP agents use the flat list, aionrs uses the
provider-grouped list; both adopt the shared structure.
Data sources and switch callbacks are untouched — selectModel / setConfigOption
/ handleSelectModel / setCurrentModel are only rewired to the shared list's
onSelect. Health dots are dropped for a consistent look across surfaces, which
also removes a stale .includes('') provider match in the Guid ACP branch.
* chore(i18n): add model search strings
Add agent.model.searchPlaceholder and agent.model.noResults across all 12
locales for the model list search box, and regenerate i18n key types.
* test(conversation): cover two-level model selector
Update the ACP and aionrs selector tests for the two-level layout: first-level
model/thought rows, submenu option lists, model search shown past the
threshold, cross-group filtering, and empty state. Include the PRD.
* refactor(conversation): fold draft box help into the mode toggle (#3553)
Drop the standalone "?" help button from the send draft box header — it took
up space of its own. The Auto/Manual mode toggle now carries the help tooltip,
so hovering it both explains the two modes and offers the switch. Mobile keeps
the help entry in the overflow menu.
* WIP: feat(team): add manual teammate management (#3532)
* feat(team): add multi-member teammate picker
Create shared Team assistant picker components, support multi-member creation with exactly one leader, add tab add-member popover, send canonical agents payloads, refresh i18n keys, and pin AionCore to ad720fa9d1e11031ea42df093a7f0944162bd41a.
* fix(team): honor manual teammate review gaps
Render backend-provided team block reasons in the shared picker and add the missing plan-required tests for manual Team creation, duplicate add-member rows, model-resolution failure, and canonical leader serialization.
* fix(team): align create modal with reference layout
* fix(team): normalize create modal density
* fix(team): clarify leader flag state
* fix(team): align add member popover style
* fix(team): tighten add member popover placement
* fix(team): remove assistant picker list fill
* fix(team): align add member picker details
* fix(team): tighten add member picker alignment
* fix(team): reduce create modal density
* fix(team): route warmup through team runtime
* fix(team): initialize selectors from team config snapshot
* fix(team): show model config loading without blocking warmup
* fix(ui): align model loading indicator
* fix(ui): center runtime loading spinner
* fix(agent): fall back when config options fail
* Gate prompt dumps behind env flag
* Wait for team warmup before config options
* fix(team): refresh config options from runtime readiness events
* fix(team): decouple config options from session warmup
* Show persisted model while config options load
* fix(team): disable member edits during warmup
* fix(team): disable member rename during warmup
* fix(team): prevent blocked assistant reason overflow
* fix(team): block teammate removal during session warmup
* Fix blocked team assistant picker layout
* test(team): align manual teammate test contracts
* feat(ui): add unified AionSearchInput and adopt it across search boxes
Extract a shared AionSearchInput base component (styled after the
conversation title search: 34px height, 12px radius, subtle gradient,
left icon, round clear button) and replace the ad-hoc search inputs in
conversation history search, workspace file tree, skills hub, guid
assistant/workspace pickers, and the team assistant picker. Width is
driven by the parent container to avoid overflow.
* fix(workspace): keep folder select trigger single-line after picking
Show only the folder name (full path moves to a title tooltip) so the
trigger height no longer grows after a folder is selected. Affects both
the team create dialog and the scheduled task dialog that share this
component.
* feat(team): polish the create-team dialog layout and copy
Tighten the header/columns spacing, add a visible column divider, and
restyle the subtitle into a short team intro. Give the selected-member
box a fixed area with a centered empty state (keeping the temporary
teammate hint) and internal scrolling, pin the team name / workspace
fields to the bottom, and switch the leader marker from a flag to a
crown to match the post-create identity. Drop the auto-focus on the
team name field and sync the copy across all locales.
* feat(ui): add AionModal standard variant for task dialogs
Introduce a 'standard' variant on AionModal that renders the unified
three-section task-dialog layout: title area / content / action bar,
each with consistent padding and a single full-width divider between
sections. Clear Arco's default content padding and any stray borders so
the sections read as one clean card split by two hairlines, and support
an optional subtitle that collapses when absent. Adopt it in the add-model
dialog as the reference and drop that dialog's dead placeholder markup.
* feat(ui): adopt AionModal standard variant in add-platform dialog
* fix(ui): stop standard modal from clipping select dropdowns
Move the corner-clipping overflow:hidden off .arco-modal-content onto the
inner wrapper. Select popups mount under .arco-modal-content but outside the
wrapper, so clipping the content cut off any dropdown overflowing the modal
edge. Clipping the wrapper instead keeps rounded corners while letting popups
float free.
* feat(ui): adopt AionModal standard variant in edit-model dialog
* feat(ui): adopt AionModal standard variant in api-key editor dialog
* fix(modal): centralize standard modal max-height
Move the height cap onto the inner .aionui-modal-wrapper
(calc(100vh - 64px), i.e. 32px top/bottom margin) so every standard
variant modal shares one source of truth: title and footer stay pinned
while the middle body scrolls, and the modal never exceeds the viewport
yet stays vertically centered as it grows.
Remove the per-component maxHeight overrides that conflicted with this
central cap and caused tall content to escape the smaller modal box.
* refactor(cron): migrate CreateTaskDialog to AionModal standard variant
Replace ModalWrapper with AionModal variant='standard' for the unified
three-section layout. Behaviour and field logic are unchanged; tests are
updated to mock AionModal and provide a ThemeContext stub.
* refactor(feedback): migrate FeedbackReportModal to AionModal standard variant
Replace ModalWrapper with AionModal variant='standard', moving the
'solve via chat' action and submit/cancel buttons into the footer render.
Behaviour is unchanged; unit and e2e tests are updated to target the new
aria-label='Close' button and scope the close selector to the visible
modal wrapper.
* refactor(settings): migrate settings modals to AionModal standard variant
Migrate DirectorySelectionModal, AgentHubModal, CssThemeModal,
JsonImportModal and OneClickImportModal to AionModal variant='standard'
for the unified three-section layout, moving footer actions into the
footer render and dropping fixed heights so content sizes naturally.
No modal logic or field interactions are changed.
* fix(conversation): drain queued messages after switch
* fix(conversation): keep assistant streaming pinned
* feat(modal): let standard variant opt out of default body padding
Standard modals apply a default 20/24 body padding. Allow a caller to pass
contentStyle.padding (e.g. 0) to take over padding itself, so full-bleed
content such as the team-create two-column layout can sit flush to the
edges while still inheriting the standard header, footer, centering and
max-height rules. Additive and scoped to the standard variant — no effect
on modals that don't pass contentStyle.padding.
* refactor(team): migrate TeamCreateModal to AionModal standard variant
Adopt variant='standard' for the unified title/footer/centering/max-height
rules while keeping the team-specific two-column body full-bleed via
contentStyle padding:0. Move title+subtitle into the standard header and
the action buttons into the standard footer, dropping the hand-written
divider and padding. The close button is now button[aria-label=Close];
update the team e2e close selectors and the unit mock accordingly. No
modal logic or field interactions changed.
* test(e2e): isolate userData into a disposable sandbox
Add an E2E-only hook in configureChromium that redirects userData to an
explicit sandbox directory when AIONUI_E2E_TEST=1 and AIONUI_E2E_USER_DATA_DIR
is set, before any getPath() call. The fixture now creates a fresh temp
userData per run and cleans it up on exit.
Previously E2E launched against the developer's real database; a shared DB
that fails migration left AionCore unable to boot, blanking the whole app.
A per-run sandbox migrates a clean DB so the app starts reliably. Guarded
by AIONUI_E2E_TEST so dev and production are unaffected.
* feat(team): add narrow-screen layout for the create-team modal
Below the 768px mobile breakpoint the create-team modal switches from the
desktop two-column layout to a single column: only the selected-members
list and team fields show inline, and the assistant picker becomes a
compact dropdown anchored to an 'Add member' button in the list header.
Picking an assistant closes the dropdown so the member immediately appears
in the list below — the 'added successfully' feedback the bottom-sheet
approach lacked. The dropdown reuses TeamAssistantPicker and the existing
select/leader logic; the desktop two-column layout is unchanged.
TeamMemberDraftList gains an optional headerAction slot so the mobile
header can host the add button without duplicating the members title.
Covers the flow with new unit and e2e tests.
* feat(ui): add AionInlineSearchInput for dropdown list search
Extract a lightweight search box (soft grey fill, borderless, 8px radius)
matching the style the home project picker used before the unified search
box landed. It reads better inside click-to-open dropdown lists than the
bordered AionSearchInput, which suits persistent search bars.
Adopt it in the three dropdown lists that carry a top search: the home
project picker, the mid-chat add-member popover, and the new-team mobile
add-member dropdown. TeamAssistantPicker gains a searchVariant prop so
only the dropdown call-sites switch to the soft style; its persistent
desktop pane keeps the bordered AionSearchInput.
* fix(team): scroll the member list on mobile and pad the assistant dropdown
On narrow screens the selected-members list had no bounded height, so a
long list overflowed the modal without scrolling and pushed the team-name
and workspace fields off screen. Give the list box a fixed max-height on
mobile (via a new optional listBoxClassName on TeamMemberDraftList) so it
scrolls internally like the desktop pane while the fields stay visible.
Also increase the assistant dropdown's inner padding so its search box no
longer sits flush against the top border.
* fix(team): render blocked-assistant tooltip above the picker dropdown
The block-reason tooltip mounted inside the modal's stacking context, so
its z-index could not beat the add-member dropdown that is portaled to
document.body — the hint rendered behind the very list it belonged to.
Portal the tooltip to document.body as well and raise its z-index above
the dropdown so the reason is always visible on hover.
* refactor(team): extract shared assistant-picker dropdown with header
Both add-member entry points — the desktop team tab-bar '+' and the
new-team modal's mobile add button — opened a click-to-show dropdown that
wraps the assistant picker. Extract TeamAssistantPickerDropdown to own the
popup's positioning, z-index, and styling so the two sites share one
implementation and only supply the anchor and select handler.
Give the dropdown an optional title/subtitle header. The team tab-bar
dropdown now shows an 'Add member' title with a short 'The same assistant
can be added repeatedly' subtitle, replacing the longer footer hint. Add
the subtitle i18n key across locales and drop the now-unused footerHint.
* docs(team): add team runtime experience PRD and technical design
Capture the team collaboration UX overhaul: member identity colors,
capsule member bar, parallel/single view toggle, warmup loading state,
add-member/tell-leader guidance, and leader greeting — with the
front-end technical design (state ownership, color allocation, warmup
gating) and staged rollout plan.
* feat(team): add member identity color system
Assign each team member instance (slot_id) a low-saturation identity
color from a fixed palette: the leader is pinned to the brand color,
new members take the smallest free slot (reusing colors freed by
removals), and colors persist per team in localStorage so a member's
color never changes when others are added or removed. Palette cycles
when members exceed its size.
Expose colorOf(slot_id) and colorOfConversation(conversation_id) through
TeamTabsContext (the latter maps a teammate message's senderConversationId
back to its member), plus a TeamIdentityContext so the deep message
render chain can read colors without prop drilling. Pure front-end, no
backend or data-model changes.
* feat(team): capsule member bar with identity-colored names and selection
Rework the top member tabs into capsules: round avatar with a
bottom-right status dot, identity-colored member name, and a neutral
background (light default, one shade darker on hover/selected) — the
identity color lives on the name text, not the capsule fill. Member
rename/remove actions reveal on hover; the add-member entry is fixed at
the far right (borderless, with a left divider) and no longer scrolls
with the list.
Chat columns carry a faint identity tint with an identity-colored header
name; the column body stays light for message readability and only the
header deepens slightly when selected. Teammate message bubbles get an
identity-colored left bar and sender name. Add a gray divider between
columns.
The per-column remove button moves out of the panel header into the
capsule (single remove entry). Drop the now-obsolete content-panel
remove test (the removal + cron-cleanup ordering stays covered by
removeTeamAssistantWithCronCleanup.test.ts and the team-member-ops e2e).
* fix(team): refine capsule bar and header colors for readability
Keep member capsules on a constant light-grey fill (hover and selection
no longer darken it, which had washed out the colored name); show the
selected capsule with a border in the member's own identity color so it
reads clearly and echoes the name. Drop the identity tint from the column
header (it muddied the colored header name) and use a lighter grey header
background; the column body keeps only a faint identity tint. Remove the
per-column selection highlight (selection now lives on the capsule). Clean
up the add-member button: no native button border box and no grey block
in its disabled state.
* feat(team): add parallel/single collaboration view toggle
Add a view toggle in the title row (labeled 'View' + parallel/single
icons): parallel shows all member columns side by side, single fills the
screen with the currently selected member. The mode is remembered per
team in localStorage (default parallel) and only shows for teams with
2+ members.
Both views share the active member (switching a tab in single view
swaps the full-screen member; a column's fullscreen button jumps to
single view for that member). Replaces the ad-hoc fullscreenSlotId state
with a persisted viewMode. Add team.view.* i18n across locales.
* feat(team): add warmup loading overlay gated on leader readiness
Show a frosted overlay over the chat area when entering a team while the
leader runtime is still starting: member avatars light up as they become
ready, with a 'waking up the team (N/M)' label and a brand-colored
progress bar. The overlay clears as soon as the leader is ready — not
waiting for every member — so a single member that fails to start never
blocks the whole team. A 20s timeout and a leader-failure state fall back
to an error view with a retry action instead of spinning forever.
The overlay covers only the chat region (title row stays usable, so view
switching still works); add/remove/rename and sending are gated during
warmup via the existing membershipMutationBusy overlap. Add team.warmup.*
i18n across locales.
* feat(team): leader greeting and tell-the-leader guidance
Give the leader's empty state a first-person greeting ('Hi, I'm the
Leader. I understand your goal and coordinate the team…') in place of the
old subtitle, keeping the three suggestion cards.
Add a guidance footer to the add-member dropdown: 'Not a good fit? You
can also ask the Leader to arrange it. Tell the Leader →'. Clicking it
switches to the leader tab and pre-fills the leader's send box with 'Help
me add a member good at ___ to the team' (not sent), so users discover
they can grow the team through the Leader, not only via the GUI. Add the
new i18n keys across locales.
* fix(team): drive warmup overlay off the team session, not runtime events
The warmup overlay almost never appeared: on team entry the backend does
not broadcast agentRuntimeStatusChanged (those fire only on add-member
rebuilds) and members' TeammateStatus is already 'idle', so the gate
resolved to ready immediately. conversation.ensureRuntime is also wrong
here — the backend rejects standalone runtime-ensure for team-owned
conversations (TEAM_RUNTIME_REQUIRED).
Gate the overlay on team.ensureSession (POST /api/teams/{id}/session)
instead — the call that actually starts the team runtime on entry and
resolves when it's ready / rejects on failure. The overlay now shows
during the real warmup and clears when the session is ready. Add a
useTeamWarmup unit test and the ensureSession mock to the cron dom test.
* fix(team): full-width tell-Leader divider and auto-scroll to active column
- Make the add-member dropdown's tell-Leader guidance a full-width top
divider + text instead of an inset that reads as a boxed footer.
- In parallel view, scroll the newly active column into view when the
active slot changes programmatically (e.g. clicking Tell Leader).
- Drop the dead per-header remove handler left over after the remove
action moved to the capsule bar.
* fix(team): drive warmup avatars off real per-member runtime signal
The warmup overlay previously lit every avatar and showed N/N instantly:
it keyed off the team member work status (all 'idle' on entry), not the
runtime warmup, so the '(ready/total)' counter was cosmetic and always full.
- useTeamWarmup now also consumes agentRuntimeStatusChanged and exposes a
per-slot runtime status map (pending/ready/failed). The overlay lights
avatars from that signal: dim → breathing 'waking up' (leader first, as
the backend rebuilds staggered) → lit. Session readiness (overlay dismiss
/ failure) still gates on the authoritative team-level ensureSession
promise, since success is emitted all-at-once, not per member.
- Drop the misleading '(ready/total)' progress line for an honest subtitle;
add a team-warmup-breathe keyframe for the pending state.
- Replace the fixed 20s timeout with a no-progress watchdog reset on each
runtime event, so large staggered teams don't false-timeout.
- Disable add/remove/rename member while warming up (PRD requires it; the
buttons live in the header which the overlay does not cover).
* feat(team): actionable warmup failure with per-member recovery
When warmup fails, the overlay now identifies the failing member and
guides recovery instead of offering a dead-end retry:
- The overlay no longer covers the column headers (starts below the 40px
header row), so each member's model selector stays clickable — switching
the model is the shared self-rescue path for both members and the lead.
- The failure card names the failing member and its error, and branches
the guidance: a teammate can switch its model or be removed from the
capsule bar; the lead (which cannot be removed) can only switch its model.
The failing member's avatar is marked in both the card and its capsule.
- Member add/remove/rename are disabled only while actively warming, not in
the failed state — previously the failed state kept them disabled, which
trapped the user (they could not remove the member blocking startup).
- useTeamWarmup now carries the per-member error alongside its status so the
card can surface the reason.
Verified live against teammate-fail and lead-fail teams on aioncore 0.1.44.
* refactor(team): tighten the warmup failure card
The card repeated the member name, role and 'failed to start' across a
title line and a reason line, and dumped the raw wrapped backend error.
- Collapse title + reason into one line that states role, name and status
once (e.g. 'Lead {name} failed to start'); use 'Leader' rather than a
translated role word in Chinese.
- Show the error in a compact danger-tinted box, and strip the backend
wrapper noise (rebuild UUID, repeated 'Invalid request/Bad request'
prefixes) down to the meaningful tail via simplifyWarmupError.
- Hints become action-only (switch model / remove then retry), dropping the
role word they used to repeat.
* style(team): apply oxfmt formatting
Run the repo formatter over the team-runtime work so it passes the
push gate's fmt-check. Whitespace/line-wrapping only, no logic change.
* fix(team): warm runtime before config changes
* feat(team): summarise multiple warmup failures in one card
The failure card only reported the first failed member. The backend can
report several failures at once (concurrent rebuild batch), so surface
all of them:
- When more than one member fails, the card title becomes a count and the
error area lists each failed member with its simplified reason (leader
tagged), capped in height and scrollable. A single failure keeps the
named-member layout.
- The recovery hint offers removal whenever any failed member is a
teammate, and falls back to model-switch-only when only the lead failed.
- Nudge the overlay top from 40 to 44px so the column header's bottom
border stays visible above the frosted layer.
Adds TeamWarmupOverlay.dom.test.tsx covering single / multi / lead-only
/ ready states.
* fix(ui): center icon-park icons and strengthen the view toggle
icon-park renders each icon as an inline span.i-icon, so the SVG sits on
the text baseline and drifts upward wherever it shares a flex row (the
view-toggle icons, the retry button icon, the capsule edit/remove icons,
and more). Make .i-icon an inline-flex centering box globally so icons sit
truly centered — one fix, everywhere.
Also make the team view toggle's selected segment fill with the brand
color and a white icon (was brand-on-white, too subtle to read as active).
* fix(ui): stop single-side borders drawing a full phantom box
UnoCSS's border-solid sets border-style:solid on all four sides, while
the single-side width utilities (border-l/border-t/…) only set width on
their own side. The other three sides then fall back to the CSS initial
border-width of medium (~1.5px) and, now being solid, become visible — so
'border-l border-solid' drew a full ~1.5px box instead of one 1px line.
This showed up as stray frames around the add-member button, the picker's
tell-Leader guidance footer, and (doubled between adjacent columns) the
chat column headers.
Add a Tailwind-Preflight-style border baseline to the global preflight:
every element defaults to border-width:0 + border-style:solid, so a
single-side utility draws exactly its one line and nothing else. Fixes all
such phantom frames app-wide, not just the team page.
* fix(skills): show empty state for sections with no search matches
When a search query filtered a skills section down to zero results, the section rendered an empty container instead of any feedback. Each section (custom, official, extension, auto-injected) now shows the dashed empty-state box with a 'no matching skills' message, matching the existing empty-state styling. Adds the noSearchResults i18n key across all locales.
* fix(guid): cap home model selector width to prevent mobile overflow
The home-screen model selector button had no width constraint, so a long model label (e.g. 'global.anthropic.claude-opus-4-8 · Default') grew the non-shrinking action group and pushed the attach (+) button out of the input row on narrow viewports. The label now truncates with an ellipsis (max 220px on desktop, 90px on mobile) and the button is allowed to shrink, keeping the attach, model, mode and send controls inside the input box.
* fix(model): clear default base URL for Dashscope Coding Plan
The Dashscope Coding Plan platform pre-filled a fixed coding endpoint in the add-model form's Base URL field. Users on this plan need to supply their own endpoint, so the platform now has an empty default and the field starts blank.
* fix(settings): align Agent tab header with other settings tabs
The Agent tab wrapped its content in an extra py-16px/px-16px layer, which conflicted with the shared page padding and the header's negative-margin bleed, leaving the Agent header slightly higher than the Model/Skills/Tools tabs and causing a visible jump when switching tabs. Removing the redundant padding aligns all tab headers to the same top offset.
* refactor(model): remove multi-API-key editor entry from add-model modal
Removes the pencil suffix on the API Key input that opened the multi-key editor dialog, and deletes the now-unused ApiKeyEditorModal. Multi-key rotation is still configured from the platform edit screen (as the inline hint states).
* fix(ui): make default border color transparent and restore team divider
Follow-up to the border baseline fix. Two regressions from it:
- Elements that set a border width/side without a color class fell back to
the CSS initial border-color (currentColor = text color), so once the
baseline forced border-style:solid they rendered as stray black lines
across many pages. Default the baseline border-color to transparent, so
an unspecified border stays invisible (as before) while explicit colors
(border-base etc.) still show.
- The team member bar drew its bottom divider only via the old phantom
border; with that gone the line between the capsule bar and the column
headers vanished. Add an explicit border-b to the bar.
Also nudge the warmup overlay top from 44 to 41px so the column header's
1px bottom border stays visible.
* fix(team): simplify the add-member disabled state to lighter text only
The disabled add-member button showed a gray fill, sat higher than the
capsule row, and read as a boxed control. Causes: it used undefined
--color-text-* tokens (empty at :root, so the muted color never applied),
and the native button's UA disabled styling (gray background + collapsed
height) bled through and won over the utility classes in the disabled
state.
Point the text at the real --text-disabled / --text-secondary tokens, and
force appearance:none + transparent background + a locked 32px height via
inline style so the UA disabled chrome cannot override it. Disabled now
differs from enabled only by lighter text — same position, size, no fill,
no border.
* style(errors): preserve original error via cause when re-throwing
Lint auto-fix: pass { cause: error } when wrapping and re-throwing in the
team create flow and the e2e renderer-mount fixture, so the original error
is retained in the chain. No behavior change.
* fix(team): gate capsule ops on warmup only, not the mutation-busy flag
Capsule add/remove/rename were disabled by membershipMutationBusy in
addition to warmup. That flag is driven by pending / session_injecting
runtime events, so removing a member (which makes the backend rebuild the
rest) re-raised it and re-locked the bar — the 'delete one, wait, delete
next' stutter, and it also re-locked after a failed warmup where ops are
supposed to be open.
Gate capsule ops on the warmup phase alone: locked only while warming,
fully open once warmup ends (ready or failed). The backend already
serializes add/remove/rename via a per-team mutex, so the frontend flag was
never a correctness guard — only a visual busy hint — and dropping it from
the gate is safe. The flag itself stays in the session/context, just no
longer feeds the capsule disable state.
* feat(team): hover affordance on capsule actions and confirm every removal
- The capsule edit/remove icons now get a rounded hover background and
color shift (were opacity-only), so they read as real buttons.
- Removing a member always asks for confirmation now (previously only when
the member was actively working); the active case keeps the stronger
'work will be interrupted' wording, and the confirm button is styled as a
danger action.
* feat(team): member greeting, identity-colored name, add-member prompt in empty state
- Non-leader members now show a greeting subtitle too ('Hi, I'm a team
member...'), matching the leader's.
- The name under the avatar renders in the member's identity color instead
of the default text color.
- Swap the leader's middle empty-state suggestion for the 'ask the Leader
to add a member good at ___' prompt, reusing the tell-Leader wording.
i18n: add emptyState.memberGreeting and emptyState.suggestions.add_member,
and reword removeAgent.confirmContent (+ confirmContentActive) across
locales.
* Drive team warmup from session events
* Use team session status for warmup
* Sync team warmup state with member controls
* fix(i18n): drop duplicate skillsHub.noSearchResults key
settings.json had two skillsHub.noSearchResults entries per locale — an
older one plus a newer translated one. JS parsing silently keeps the last,
so it slipped through local checks, but CI's strict check-json rejects
duplicate keys. Remove the older orphan and keep the fully-translated
later entry (matches the code's defaultValue). No behavior change.
* fix(team): retry dynamic member warmup in place
* refactor(team): align UI with slot work coordination
* fix(conversation): preserve manual queue mode in background
---------
Co-authored-by: zynx <>
Co-authored-by: Waili(瓦砾) <icey.liu1994@gmail.com>
Co-authored-by: szafranski <p.fabiszewski@gmail.com>
* feat(guid): move mobile home input controls into a + action sheet (#3554)
* feat(chat): add multi-select mode to MobileActionSheet
Add a `multiSelect` option to action-sheet submenus. Multi-select submenus
render checkboxes (driven by `option.active`) and, on tap, toggle the selection
via `onSelect` while keeping the submenu open so several items can be toggled in
a row — distinct from single-select (radio, slides back) and action rows
(no marker, closes the sheet). Used for Skills / MCP on the home page.
Backward compatible: existing submenus without `multiSelect` are unchanged.
Add focused tests for single-select, multi-select, and action-row behaviour.
* feat(guid): move mobile input controls into a + action sheet
On mobile, the home page input row previously laid model and permission
selectors out inline (with a hover-dropdown + for files/skills/MCP), which was
cramped and needed horizontal scrolling. Collapse everything into the + button:
tapping it opens the bottom action sheet with model / thought level (ACP only)
/ permission (single-select), add files, and skills / MCP (multi-select
checkboxes, showing a selected count on the main row).
Desktop is unchanged — it keeps the inline selectors and the hover dropdown.
GuidPage passes the flat model/thought data down so GuidActionRow can build the
sheet entries; switch callbacks are reused as-is.
* refactor(conversation): rename loaded skills/MCP to selected
In the conversation + menu (mobile action sheet and desktop dropdown), the
skills and MCP entries listed what is currently active, labelled "Skills" /
"Loaded MCP" / "Loaded Skills". Rename them to "Selected skills" / "Selected
MCP" so the wording reflects that these are the chosen items — distinct from the
home page, where the same controls are the picker itself ("Skills" / "MCP").
The conversation skills indicator keeps "Loaded skills" (it reports live state).
* chore(i18n): add selected skills/MCP and selected-count strings
Add common.selectedSkills, common.selectedCount ({{count}}) and
conversation.mcp.selected across all 12 locales for the home-page action sheet
and the renamed conversation skills/MCP entries. Regenerate i18n key types.
* feat(i18n): add French locale (#2731)
Co-authored-by: zk <zk@users.noreply.local>
* chore: bump version to 2.1.32 and aioncore to v0.1.45 (#3555)
Co-authored-by: zynx <>
* fix(build): verify current Codex ACP Windows binaries (#3557)
Co-authored-by: zynx <>
* fix(build): align Codex installer verifier (#3561)
- Check the OpenAI Codex platform binary layout currently bundled by AionCore on Windows
- Add a focused regression test so the stale Zed Codex ACP path is not reintroduced
* chore: bump version to 2.1.33 (#3562)
Co-authored-by: zynx <>
* fix(conversation): prevent queue drain from racing backend idle state (#3571)
Three related 409-CONFLICT scenarios were identified where the draft-box
queue fires a message before the backend has fully released the conversation:
1. After a turn completes naturally, the execution gate flips canExecute
based on an optimistic local runtime snapshot that may precede the
backend actually becoming idle.
2. After the user clicks Stop, the queue drain fires immediately once
handleStop resolves, but the backend may still be processing the abort.
3. "Send Now" called sendNow() directly after effectiveHandleStop(),
bypassing the execution gate entirely.
Additionally, when onExecute was called before the state update settled,
a message could disappear from the draft box without being confirmed sent
(remove and restore mutate calls raced each other).
Fixes:
- Add prioritize() to useConversationCommandQueue: moves a command to the
front of the queue and ensures mode=auto, letting the drain effect send
it through the normal gate rather than bypassing it.
- Replace sendNow() in handleSendNowQueued (both AcpSendBox and
AionrsSendBox) with effectiveHandleStop() + prioritize(), so "Send Now"
honours the execution gate and waits for the backend to confirm idle.
- In the foreground drain effect, await updateState before calling
onExecute so the item only leaves the UI once the send is accepted (or
is restored on failure), eliminating the remove/restore mutate race.
- On 409 CONFLICT (isConversationBusyError) in both the foreground drain
catch and drainBackgroundCommandQueue, restore the command without
pausing the queue and re-trigger the effect (via setExecutionGateVersion
or a short setTimeout retry) instead of surfacing an error to the user.
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
* fix(team): show accepted team work as processing (#3576)
Co-authored-by: zynx <>
* chore: bump version to 2.1.34 and aioncore to v0.1.46 (#3578)
Co-authored-by: zynx <>
* fix(startup): classify assistant bootstrap failures (#3583)
Co-authored-by: zk <zk@users.noreply.local>
* fix(feedback): attach team route context
* test(feedback): cover blank route identifiers
* chore(docs): update WeChat group QR code to wx-16 (#3588)
- Replace wx-15.png with wx-16.png across all README files
- Update references in main README and localized versions
- Remove outdated wx-15.png image
* fix(packaging): verify bundled resources from manifest (#3587)
* fix(packaging): verify bundled resources from contract
Consume AionCore managed-resources/manifest.json in packaging and Windows install verification. Reject missing, invalid, duplicate, or old-version-only managed Node and ACP resources, and update the legacy ACP artifact script to current package pins.
* test(build): strengthen managed resource verifier gates
Reject non-numeric managed resources schemaVersion values in the Windows installer verifier and add focused prepare-aioncore gate coverage for Actions artifacts, GitHub release downloads, and local binary fallback inputs.
* test(packaging): skip POSIX fake aioncore gates on Windows
* chore(packaging): bump bundled AionCore to v0.1.47
---------
Co-authored-by: zynx <>
* fix(conversation): handle busy send conflicts (#3589)
* fix(conversation): handle busy send conflicts
Classify AionCore 409 conversation busy responses as non-retryable conversation busy errors, preserve the runtime send gate for busy failures, and suppress internal error cards in ACP, initial ACP, and Aionrs send paths.\n\nRestore queued commands on busy conflicts and wait for the runtime gate to observe a blocked state before draining again, while keeping foreground and background queue runners from racing after execute callback identity changes.\n\nAdd focused regression coverage for busy classification, runtime gate preservation, command queue restore/wait, ACP/Aionrs send paths, initial message handling, and team safety.
* fix(conversation): preserve observed busy queue gate
Keep the foreground queue busy-wait latch aware of a blocked runtime gate that was already observed before a 409 busy rejection reaches the queue catch path. This prevents restored queued commands from getting stuck after the gate later becomes sendable.
Add a regression test for the pre-observed blocked-gate timing.
---------
Co-authored-by: zynx <>
* fix(renderer): restrict message file marker parsing (#3590)
Preserve literal [[AION_FILES]] text in assistant, left-side, and teammate messages. Only parse right-side user attachment blocks when the marker is standalone and followed by local-looking file paths.
Co-authored-by: zynx <>
* chore: bump version to 2.1.35 (#3591)
Co-authored-by: zynx <>
* feat(cron): add queue protection and custom schedules (#3552)
Co-authored-by: zk <zk@users.noreply.local>
* chore(deps): remove aioncli-core dependency (#3594)
Co-authored-by: zk <zk@users.noreply.local>
* chore(deps): remove office-ai platform dependency (#3595)
Co-authored-by: zk <zk@users.noreply.local>
* feat(skills): add batch delete for custom skills (#3600)
* feat(skills): add batch delete for custom skills
Add a batch-manage mode to the Custom skills tab: multi-select via
checkboxes or card click, select-all over the filtered list, and a
single confirmation to delete the selected skills. Reuses the existing
single-delete REST endpoint per skill (no backend changes); partial
failures surface a warning with success/failure counts. Batch mode
resets on tab switch. Adds i18n keys across all 13 locales and unit
coverage for enter/select/delete/partial-failure/cancel flows.
* fix(settings): add top spacing to mobile settings nav
The horizontal pill nav on mobile settings pages sat flush against the
title bar. Add 14px top padding so it no longer looks clipped.
* test(e2e): cover skills batch delete and repair stale skills helpers
Add TC-S-05b covering the batch delete flow end-to-end (enter batch
mode, select two of three skills, confirm, verify backend state).
Repair two stale bits in the skills E2E helpers that broke the whole
suite's navigation and setup:
- goToSkillsHub still targeted /settings/capabilities, which no longer
exists since skills/tools were split into standalone entries; point
it at /settings/skills.
- importSkillViaBridge sent camelCase skillPath while the backend
expects snake_case skill_path, so every bridge import silently
failed with 400.
* style(skills): soften batch-mode selected card state (#3603)
Replace the primary border + tinted background on selected cards with
a borderless light fill, so selection reads from the checkbox and
background alone without a heavy outline.
* feat(skills): skill detail page with assistant attachment (#3604)
* refactor(skills): group skills settings files into SkillsSettings directory
pages/settings/ exceeded the 10-child directory limit; move
SkillsHubSettings.tsx and skillImportMessages.ts into a SkillsSettings/
module directory ahead of adding the skill detail page.
* feat(skills): add skill detail page with assistant attachment
New /settings/skills/detail/:skillName route styled after the assistant
editor page. A single used-by list is the source of truth for the
skill-assistant relation: an 'Attach to assistant' dropdown lists only
assistants not yet using the skill, each row exposes an inline remove
and links to the assistant editor. Attach/detach reuses the existing
PUT /api/assistants/:id partial update (enabled_skills only) — no
backend changes. Builtin assistants are listed read-only since their
update path only accepts agent/defaults fields. Adds i18n keys across
all 13 locales and unit coverage for add-menu filtering, attach,
inline detach, and builtin read-only handling.
* feat(skills): link list cards to detail page and show used-by avatars
Skill cards (custom and official) navigate to the new detail page on
click; in batch mode the card click still toggles selection. Cards with
attached assistants show a compact overlapping avatar stack (+N
overflow, tooltip with names), mirroring the agent page's bound-
assistant stack. Per-card delete stops propagation so it no longer
triggers navigation.
* feat(ui): add search to skills/MCP submenus and assistant default selects (#3605)
* refactor(agent): generalize model-list search threshold and scroll class for reuse
* feat(guid): add search to skills and MCP submenus in the plus menu
* feat(assistants): make default model/skills/MCP selects searchable
* test(e2e): cover dropdown search in plus menu and assistant editor
* feat(ui): standardize drag-to-reorder UX for team tabs and pinned conversations (#3606)
* feat(conversation): drag pinned conversations via hover handle with in-place drop
* feat(team): rewrite member tab reordering with dnd-kit sortable
* refactor(chat): reuse shared vertical-axis drag modifier in command queue
* chore(i18n): regenerate i18n key types
* fix(build): resolve app-builder-lib within the current repo when patching NSIS template
* chore: bump version to 2.1.36 and aioncore to v0.1.48 (#3610)
Co-authored-by: zynx <>
* fix(bridge): accept void-param invokes after JSON serialization (#3611)
* fix(bridge): accept void-param invokes after JSON serialization
Zero-argument bridge invokes (window-controls:minimize/maximize/close,
autoUpdate.checkForUpdates, application.restart, etc.) send
`data: undefined`, which JSON.stringify drops from the wire payload on
Electron IPC and WebSocket transports. The subscribe guard introduced in
#3595 required the `data` key to be present, so every zero-arg request
was silently discarded — on Windows this broke the custom titlebar
minimize/maximize/close buttons in v2.1.36 (Sentry ELECTRON-3JZ).
Drop the `'data' in request` requirement and read the property
optionally; add a unit test that round-trips the request through JSON
like the real transports do.
* test(e2e): cover window-controls zero-arg invokes over real IPC
Regression coverage for the v2.1.36 bridge bug: drive
window-controls:is-maximized/maximize/unmaximize through the real
Electron IPC transport and assert the BrowserWindow state changes from
the main process. Both tests fail on the pre-fix bridge guard.
* fix(renderer): keep team elapsed timer continuous across remount (#3612)
The team (assistant-preset) member-column processing timer in
ThoughtDisplay was implemented as a local counter seeded from
Date.now() on mount and reset on every timing useEffect re-run. Any
subtree remount or dependency churn (opening settings and returning,
toggling fullscreen/windowed, resizing the window) reinitialized the
origin to "now", so the elapsed time restarted from 0s
(Sentry ELECTRON-3J5 / 134761572).
Drive the elapsed time from the backend-provided absolute start
timestamp (ITeamSlotWork.active_turn_started_at_ms) along the team
chain so it survives remounts:
- ThoughtDisplay: add startedAtMs and externalElapsedSource props and a
three-state timer. State A (external mode + valid timestamp) computes
elapsed as now - startedAtMs, recomputing from the same origin after
remount. State B (external mode + invalid timestamp) suppresses the
number and shows only status text and spinner. State C (non-external)
keeps the existing local useRef(Date.now()) behavior, so non-team
sessions are unchanged.
- teamSendRuntime: expose startedAtMs from work.active_turn_started_at_ms
(null-normalized) on TeamSendBoxRuntime.
- AcpSendBox / AionrsSendBox: forward externalElapsedSource and
startedAtMs to ThoughtDisplay, gated on the presence of teamRuntime.
Add focused Vitest coverage for the new behavior (external timestamp,
remount continuity, per-second advance, invalid-timestamp suppression,
new-turn recompute, non-team local fallback, and runtime timestamp
propagation). No backend, schema, or i18n changes.
Co-authored-by: zynx <>
* chore: bump version to 2.1.37 (#3613)
Co-authored-by: zynx <>
* feat(settings): add agent and assistant search (#3616)
* feat(settings): add agent and assistant search
* feat(settings): add scheduled task search
* fix(settings): prevent scheduled tasks header bleed-through
* fix(settings): keep scheduled tasks scrolling local
* fix(settings): isolate scheduled task header scroll
---------
Co-authored-by: zk <zk@users.noreply.local>
* fix(settings): hide agent search on mobile (#3617)
Co-authored-by: zk <zk@users.noreply.local>
* fix(team): treat idle-stopped session as recoverable, not a draft-box block (#3618)
After an idle-clean reclaim, sending a team message enqueued it to the draft
box and never drained, because `session_stopped` was classified as a fatal
block (`canSendMessage=false` → `isBusy=true` → `enqueue()`), and the only
recovery call lives in the skipped `executeCommand` path. The stopped state
was also unreliable, derived from a stale slot value.
Make the stopped state recoverable-and-sendable and drive it from a reliable
event, so the existing lazy-recovery send path fires.
- types: add `'stopped'` to the `TeamSessionStatus` union.
- useTeamRunView: add an event-driven `sessionStopped` flag set by
`sessionStatusChanged('stopped')`, cleared on `starting`/`ready` and on any
applied active run event (self-heal), independent of reconciled slot work.
- teamSendRuntime: drop `session_stopped` from `FATAL_BLOCK_REASONS` (a stale
stopped slot no longer blocks sending, still shows stopped text); add a
`sessionStopped` option that forces `canSendMessage=true` and `loading=false`.
- TeamChatView: wire the flag through and prefer the stopped prompt so it shows
even when the stale slot has no blocked_reason.
- useTeamWarmup / teamMembershipMutationBusy: handle `'stopped'` explicitly
(keep warmup phase; treat as non-mutation-busy).
- tests: cover the recoverable-stopped runtime, the event-driven flag and its
self-heal, membership non-busy handling, and update the send-box mapping test
for the intentional `session_stopped` behavior change.
Co-authored-by: zynx <>
* fix(installer): run arch check before registry mutation (#3619)
- E1040 arch guard lived in .onVerifyInstDir, which fires after customInit
has already healed/cleared/repaired an existing install's registry; an
arm64 build run on an x64 machine thus mutated the correct-arch install's
registry and uninstaller before aborting
- Move the guard into preInit (after session-begin, before active-marker
writes and customInit) so a wrong-arch installer exits without touching
any registry or uninstaller state
- Report the real detected arch (arm64/x64/x86 via IsWow64Process2) instead
of the hardcoded actual=non-arm64, so triage can tell x64 from x86
- x64/arm64 rejection policy is unchanged; only timing and diagnostics change
Refs Sentry ELECTRON-3BX
* fix(system): let backend own keep-awake blocker (#3620)
Co-authored-by: zk <zk@users.noreply.local>
* feat(guid): expand assistant more dropdown into responsive multi-column panel (#3621)
* feat(guid): expand assistant more dropdown into responsive multi-column panel
* feat(guid): open assistant overflow panel on pill bar hover
* feat(guid): add subtle entry animation to assistant overflow panel
* feat(guid): hide overflow search until assistant list exceeds five rows
* feat(guid): task-oriented default prompts with refined suggestion styling (#3622)
* feat(guid): replace default assistant prompts with task-oriented suggestions
* style(guid): restyle prompt suggestions as plain text with hover arrow
* style(chat): use corner-right-up icon for send buttons
* chore: bump version to 2.1.38 and aioncore to v0.1.49 (#3625)
Co-authored-by: zynx <>
* fix(settings): keep agent repair panel mounted during background revalidation (#3624)
* chore(docs): update WeChat group QR code to wx-17 (#3623)
* chore(docs): update WeChat group QR code to wx-17
* fix(docs): rebuild wx-17 with proportional resize (no distortion)
* fix(docs): tighten wx-17 crop so QR fills frame like the right side
* fix(chat): restore arrow-up icon on send buttons (#3627)
* feat(feedback): route-aware module preselection and ask-the-butler chip on error surfaces (#3626)
* fix(feedback): map every route to its feedback module for the titlebar report button
The titlebar report button only knew 3 routes (conversation/team/settings),
so scheduled tasks, assistants and every settings tab fell through to no
module or a blanket system-settings. Extract the mapping into
resolveFeedbackModule with full route coverage; a unit test walks Router.tsx
so future pages cannot silently miss a module.
* feat(feedback): add ask-the-butler chip to conversation error surfaces
Every conversation error (structured tips, plain/JSON error tips, tool
failures, agent status errors) now shows a Butler chip next to the report
pill. Clicking routes to the home chat with the Butler selected and a
diagnosis prompt containing the error text pre-filled — same flow as the
report modal's solve-via-chat action.
The Butler chip shows on all errors, including user-environment ones that
opt out of reporting via feedback_recommended=false: those are exactly what
the Butler diagnoses best, while the report pill stays hidden for them.
* test(e2e): extend ACP stream injector with error fabrication and cover butler chip
Add emitErrorTip/emitToolError/emitAgentStatusError to the E2E stream
injector so tests can fabricate the three conversation error surfaces
without a real broken agent, and add a spec that walks the butler chip
end to end (error bubble -> home chat with diagnosis prompt seeded).
* test(e2e): repair drifted feedback specs
Four cases were failing on main: the About row label changed
(bugReport i18n copy), the custom-agent editor entry became a
TalkToButlerButton dropdown, and scenarios 5/6 asserted a feedback pill
that was deliberately removed from InlineAgentEditor in #3448. Update
selectors and expectations, and scope modal lookups to the visible
instance since multiple FeedbackReportModal mounts coexist.
* feat(settings): promote Kimi/Moonshot placement in platform and agent lists (#3629)
* feat(settings): move Moonshot platforms to top of model platform list
* feat(settings): pin Kimi after the builtin agent in the Agents list
* feat(settings): default add-model platform to the first list entry
* fix(preview): render distinct heading texts in markdown preview (#3630)
Streamdown's built-in heading components are memoized by node position
only and ignore children, so headings kept stale text when positions
were dropped (rehype-raw) or identical across re-renders. Override
h1-h6 with plain components that keep the built-in classes but always
render the current text.
* feat(github): automated issue/PR/discussion triage to module owners (#3631)
* feat(github): add module dropdown to bug and question issue templates
* feat(github): auto-label and auto-assign issues by module via triage workflow
* feat(github): add CODEOWNERS for module-based PR review routing
* feat(github): welcome-comment and mention duty owner on new discussions
* style(github): apply oxfmt formatting to triage map
* fix(github): use English-only module dropdown with exact-match triage parsing (#3636)
* docs(readme): add AionUi × Kimi contributor campaign section (#3643)
* docs(readme): add AionUi × Kimi contributor campaign section
* docs(readme): simplify campaign CTA and relocate Kimi links to platforms section
* docs(readme): inline campaign CTA and move Kimi links to K3 promo line
* docs(readme): emphasize campaign CTA, de-emphasize Kimi promo links
* docs(readme): banner as sole headline, single CTA, footnote promo
* docs(readme): restore Chinese caption as translation for English banner
* docs(readme): restore English campaign caption for SEO/bot indexing
* docs(readme): full campaign section for all 8 remaining locales
* docs(readme): link campaign CTA to X announcement and claim thread
* fix(agent-settings): hide launch path for npx agents and fix repair-panel status banner (#3641)
Two issues in the agent repair panel (AgentRepairPanel):
1. resolveDiagnosticBanner mapped any status other than missing/offline to the
"Connected" success banner, so an `unchecked` agent showed "Connected" in
the panel while the list correctly showed "Not checked". Handle `online`
explicitly and show a neutral info banner for `unchecked` that points the
user to Test Connection (top-right). While unchecked, the panel now shows
only that banner — launch path, env vars and Save & Test are hidden until a
check has run.
2. The launch-path override was offered for npx-bridged agents. Overriding it
forwards the npx wrapper args to the resolved binary and breaks startup, so
it is now hidden for bridge-launched rows (bridge_binary != binary_name),
matching the backend rejection.
Adds settings.repair.uncheckedTitle/uncheckedHint across all 13 locales and
regenerates i18n key types.
Co-authored-by: zk <zk@users.noreply.local>
* fix(workspace): stable file tree — expand state, search, preview panel (#3642)
* fix(workspace): restore pre-search expanded state on search clear
Searching a broad term opens dozens of directories; clearing the box
previously called refreshWorkspace() with all those dirs still in
expandedKeys, causing a burst of parallel IPC fetches and visible jank.
Now useWorkspaceSearch snapshots expandedKeys just before the first
search keystroke and restores it when the box is cleared, so the
refresh only refetches the small set of dirs that were open originally.
* feat(workspace): incremental tree refresh and per-project expansion cache
applyFreshListings: refresh re-fetches root + all expanded dirs in parallel
and splices fresh content into the existing tree. Expanded dirs stay open and
newly created files appear immediately — no more collapse-on-refresh and no
more stale children blocking new entries.
workspaceTreeCache: module-level LRU (20 entries) keyed by workspace path.
The tree and expandedKeys snapshot survives React remounts triggered by SWR
revalidation, so switching conversations within the same project restores the
exact tree state without a visible flash or reset.
Also includes buildSearchTree (frontend recursive file-name search at any
depth) and collectExpandedDirs (used by the refresh logic to identify dirs
that need a re-fetch).
* refactor(workspace): rewrite tree and events hooks for stable cross-conversation state
useWorkspaceTree: hydrates initial state from workspaceTreeCache on mount;
writes the cache snapshot on unmount and after every successful refresh (not
on every expand/collapse, which caused jank in large workspaces); detects
workspace path changes without remount and re-seeds from the cache.
useWorkspaceEvents: now tracks workspace path instead of conversation_id.
Same-workspace conversation switches only clear selection and call
refreshWorkspace; different-workspace switches also bump treeKey to force
re-render. Removes the backend search provider listener (search moved to
frontend).
* feat(preview): keep file preview open across same-workspace conversation switches
PreviewContext gains closePreviewIfWorkspaceChanged, backed by a stable ref
(lastWorkspaceRef) that survives React remounts. Switching to a conversation
in the same project keeps the preview panel open; switching to a different
workspace or project closes it.
Layout.tsx closes the preview unconditionally when the user navigates away
from /conversation/ routes (to team, settings, etc.), bypassing the workspace
comparison which would no-op when lastWorkspaceRef was never set on those
routes.
* perf(workspace): eliminate per-node O(N) expand lookup and inline renderer churn
expandedKeys.includes() inside renderTitle did a linear scan of the
expandedKeys array for every visible node on every render; with 100+
expanded dirs this meant tens-of-thousands of iterations per
expand/collapse, causing visible jank.
Fix: derive expandedKeysSet (Set<string>) from expandedKeys via useMemo
so FileTypeIcon gets an O(1) .has() check instead.
Also memoize treeData (flattenSingleRoot), treeIcons, and renderTitle
with useMemo/useCallback so Arco Tree does not rebuild all node
renderers on every expandedKeys state update.
* revert: perf(workspace): eliminate per-node O(N) expand lookup and inline renderer churn
Reverts commit 46984d606. The memoization changes broke existing
interaction behaviors; reverting to restore correct functionality.
* fix(preview): correct cross-workspace close behavior for teams and conversations
Bug 1 — preview not closing between teams with different workspaces:
TeamPageContent now calls closePreviewIfWorkspaceChanged(effectiveWorkspace)
whenever its workspace changes, mirroring what conversation/index.tsx does.
lastWorkspaceRef in PreviewContext is updated on every team navigation, so
switching from team A (/foo) to team B (/bar) correctly triggers a close.
Bug 2 — preview incorrectly closing when switching between conversations
of the same workspace:
Sider.handleConversationSelect was unconditionally calling closePreview()
on every conversation click. Removed that call. The workspace-aware
closePreviewIfWorkspaceChanged in conversation/index.tsx is the sole
decision point: same workspace → keep open, different workspace → close.
* refactor(workspace): remove dead mergeLoadedChildren code
applyFreshListings supersedes mergeLoadedChildren; no callers remain.
* feat(workspace): restore multi-tab file preview
Clicking a second file now opens a new preview tab instead of
replacing the first, matching the previous behavior before the
workspace refactor.
* fix(workspace): persist lazy-loaded children and fix minor memory leaks
- loadMore now writes the updated tree to workspaceTreeCache so lazy-
expanded subdirs survive a subsequent refresh or cache rehydration
- flattenSingleRoot is now memoized (useMemo) to avoid a redundant
full-tree traversal on every expand/collapse re-render
- setLoadingHandler stores the debounce timer in a ref and clears it
on unmount, preventing setState-on-unmounted-component warnings
* test: add PreviewContext mock to Layout and TeamPage DOM tests
Layout.tsx and TeamPage.tsx now call usePreviewContext; mock it in
unit tests that render these components without a PreviewProvider.
* fix(conversation): show sign-in hint for empty ACP turns needing auth (#3644)
Adds the ACP_EMPTY_TURN_NEEDS_AUTH agent-tip copy (with a {{hint}} param for the
agent's advertised login instruction) across all 13 locales, and regenerates the
i18n key types. Renders the actionable hint the backend now emits when an agent
ends a turn empty because it isn't signed in, replacing the blank "no visible
reply" tip.
Co-authored-by: zk <zk@users.noreply.local>
* feat(settings): configure model capabilities (#3639)
* feat(settings): configure model capabilities
* fix(settings): preserve automatic vision detection
* fix(settings): use dropdowns for model capability choices
* test(settings): cover model capability configuration
* test(settings): cover capability fallback branches
* fix(github): never auto-assign bonus-labeled issues in triage workflow (#3647)
* feat(github): post claim invitation when an issue is labeled bonus (#3649)
* fix(startup): stop false "local data repair failed" alarm from concurrent startup (#3650)
A second AionUi instance that lost the single-instance lock still registered
the backend startu…
acetaxxxx
added a commit
to acetaxxxx/AionUi
that referenced
this pull request
Aug 27, 2026
* feat(conversation): rework message queue into a send draft box (#3547)
* feat(conversation): add auto/manual send mode and send-now to command queue
Extend the queue state with a per-conversation send mode (auto | manual,
defaults to auto) and two new actions:
- sendNow(commandId): dequeue and execute a single queued command
immediately, leaving the rest of the queue (mode, order, paused flag)
untouched.
- toggleMode(): switch between auto (drain automatically after each turn)
and manual (hold queued messages until sent one by one).
Manual mode gates the auto-drain effect so nothing sends on its own. The
mode is persisted alongside the queue in session storage and preserved
across all state updates.
* feat(conversation): redesign command queue as send draft box
Rework the queue panel into a 'draft box' with clearer, always-visible
controls:
- Each queued message now shows three inline actions: send now, edit and
remove (edit was previously hidden in the overflow menu).
- Send now interrupts the current reply, then sends that message; wired in
both the ACP and aionrs send boxes via handleSendNowQueued.
- Header row shows the draft box title with a count badge, an auto/manual
mode toggle, a help tooltip explaining both modes, and an overflow menu.
- Clear moved into the overflow menu with a danger-styled confirm dialog.
- Narrow / mobile layout: title collapses to an icon, help folds into the
overflow menu, and rows are reordered by long-pressing the whole row
instead of a hover-only handle.
* chore(i18n): add send draft box strings
Rename the command queue title to 'draft box', rename clear to 'clear
draft box', and add keys for the send-now action, auto/manual mode labels,
mode toggle, help text and the clear confirm dialog across all 12 locales.
Regenerate i18n key types.
* test(conversation): cover send draft box mode and send-now
Update the panel test for the new three-action layout, mode toggle and
confirm-based clear. Add hook tests covering the default auto mode, toggle,
manual mode not auto-draining, resuming auto drain, sendNow targeting a
single command, and manual mode restored from storage. Include the PRD.
* chore: ignore local .analysis scratch directory
* fix(i18n): update Russian localization (#3541)
Co-authored-by: Dr.LMA <lma@mbp.lan>
* feat(conversation): rework model selector into a two-level menu (#3550)
* feat(agent): add two-level runtime selector building blocks
Shared pieces for the model selector's two-level dropdown, reused by every
model selector (ACP, aionrs, Guid), so the dropdown look & behaviour lives in
one place:
- RuntimeSelectorModelList: model options with optional provider grouping and
a search box shown once the model count exceeds MODEL_SEARCH_THRESHOLD (5),
filtering across groups. It owns a single scroll container with the search
box fixed above it (outside the scroll area) so the search box never moves or
leaves a gap; grouped provider titles stick to the top while scrolling.
- RuntimeSelectorSubMenuTitle: a first-level row (label + current value); no
chevron of its own since Menu.SubMenu draws the expand arrow.
- RUNTIME_SUBMENU_TRIGGER_PROPS: opens submenus to the left with auto-flip.
- AionInlineSearchInput: lightweight dropdown search box mirroring the one from
#3532 so it can be swapped for the shared component once that merges.
CSS: disable the Arco popup's own scroll/height cap wherever it hosts the list
(keyed off :has(.runtime-model-scroll)) so there is exactly one scrollbar, and
pin provider group titles to the top of that scroll container.
Drop the now-unused renderThoughtLevelMenuGroup and RuntimeSelectorMenuDivider.
* feat(conversation): two-level model selector across all surfaces
Rework the model dropdown from one long menu (thought-level group + model
group stacked) into a two-level menu: a first level with just Model and
Reasoning-effort rows, each expanding into a left-side submenu with the full
option list. The first level stays short no matter how many models there are.
- AcpModelSelector (Claude/Codex/Gemini CLI, single + group chat): two-level
when a thought level exists; otherwise the dropdown is the model list itself.
- AionrsModelSelector (Aion CLI conversation): same two-level structure, model
submenu keeps provider grouping.
- GuidModelSelector (home): ACP agents use the flat list, aionrs uses the
provider-grouped list; both adopt the shared structure.
Data sources and switch callbacks are untouched — selectModel / setConfigOption
/ handleSelectModel / setCurrentModel are only rewired to the shared list's
onSelect. Health dots are dropped for a consistent look across surfaces, which
also removes a stale .includes('') provider match in the Guid ACP branch.
* chore(i18n): add model search strings
Add agent.model.searchPlaceholder and agent.model.noResults across all 12
locales for the model list search box, and regenerate i18n key types.
* test(conversation): cover two-level model selector
Update the ACP and aionrs selector tests for the two-level layout: first-level
model/thought rows, submenu option lists, model search shown past the
threshold, cross-group filtering, and empty state. Include the PRD.
* refactor(conversation): fold draft box help into the mode toggle (#3553)
Drop the standalone "?" help button from the send draft box header — it took
up space of its own. The Auto/Manual mode toggle now carries the help tooltip,
so hovering it both explains the two modes and offers the switch. Mobile keeps
the help entry in the overflow menu.
* WIP: feat(team): add manual teammate management (#3532)
* feat(team): add multi-member teammate picker
Create shared Team assistant picker components, support multi-member creation with exactly one leader, add tab add-member popover, send canonical agents payloads, refresh i18n keys, and pin AionCore to ad720fa9d1e11031ea42df093a7f0944162bd41a.
* fix(team): honor manual teammate review gaps
Render backend-provided team block reasons in the shared picker and add the missing plan-required tests for manual Team creation, duplicate add-member rows, model-resolution failure, and canonical leader serialization.
* fix(team): align create modal with reference layout
* fix(team): normalize create modal density
* fix(team): clarify leader flag state
* fix(team): align add member popover style
* fix(team): tighten add member popover placement
* fix(team): remove assistant picker list fill
* fix(team): align add member picker details
* fix(team): tighten add member picker alignment
* fix(team): reduce create modal density
* fix(team): route warmup through team runtime
* fix(team): initialize selectors from team config snapshot
* fix(team): show model config loading without blocking warmup
* fix(ui): align model loading indicator
* fix(ui): center runtime loading spinner
* fix(agent): fall back when config options fail
* Gate prompt dumps behind env flag
* Wait for team warmup before config options
* fix(team): refresh config options from runtime readiness events
* fix(team): decouple config options from session warmup
* Show persisted model while config options load
* fix(team): disable member edits during warmup
* fix(team): disable member rename during warmup
* fix(team): prevent blocked assistant reason overflow
* fix(team): block teammate removal during session warmup
* Fix blocked team assistant picker layout
* test(team): align manual teammate test contracts
* feat(ui): add unified AionSearchInput and adopt it across search boxes
Extract a shared AionSearchInput base component (styled after the
conversation title search: 34px height, 12px radius, subtle gradient,
left icon, round clear button) and replace the ad-hoc search inputs in
conversation history search, workspace file tree, skills hub, guid
assistant/workspace pickers, and the team assistant picker. Width is
driven by the parent container to avoid overflow.
* fix(workspace): keep folder select trigger single-line after picking
Show only the folder name (full path moves to a title tooltip) so the
trigger height no longer grows after a folder is selected. Affects both
the team create dialog and the scheduled task dialog that share this
component.
* feat(team): polish the create-team dialog layout and copy
Tighten the header/columns spacing, add a visible column divider, and
restyle the subtitle into a short team intro. Give the selected-member
box a fixed area with a centered empty state (keeping the temporary
teammate hint) and internal scrolling, pin the team name / workspace
fields to the bottom, and switch the leader marker from a flag to a
crown to match the post-create identity. Drop the auto-focus on the
team name field and sync the copy across all locales.
* feat(ui): add AionModal standard variant for task dialogs
Introduce a 'standard' variant on AionModal that renders the unified
three-section task-dialog layout: title area / content / action bar,
each with consistent padding and a single full-width divider between
sections. Clear Arco's default content padding and any stray borders so
the sections read as one clean card split by two hairlines, and support
an optional subtitle that collapses when absent. Adopt it in the add-model
dialog as the reference and drop that dialog's dead placeholder markup.
* feat(ui): adopt AionModal standard variant in add-platform dialog
* fix(ui): stop standard modal from clipping select dropdowns
Move the corner-clipping overflow:hidden off .arco-modal-content onto the
inner wrapper. Select popups mount under .arco-modal-content but outside the
wrapper, so clipping the content cut off any dropdown overflowing the modal
edge. Clipping the wrapper instead keeps rounded corners while letting popups
float free.
* feat(ui): adopt AionModal standard variant in edit-model dialog
* feat(ui): adopt AionModal standard variant in api-key editor dialog
* fix(modal): centralize standard modal max-height
Move the height cap onto the inner .aionui-modal-wrapper
(calc(100vh - 64px), i.e. 32px top/bottom margin) so every standard
variant modal shares one source of truth: title and footer stay pinned
while the middle body scrolls, and the modal never exceeds the viewport
yet stays vertically centered as it grows.
Remove the per-component maxHeight overrides that conflicted with this
central cap and caused tall content to escape the smaller modal box.
* refactor(cron): migrate CreateTaskDialog to AionModal standard variant
Replace ModalWrapper with AionModal variant='standard' for the unified
three-section layout. Behaviour and field logic are unchanged; tests are
updated to mock AionModal and provide a ThemeContext stub.
* refactor(feedback): migrate FeedbackReportModal to AionModal standard variant
Replace ModalWrapper with AionModal variant='standard', moving the
'solve via chat' action and submit/cancel buttons into the footer render.
Behaviour is unchanged; unit and e2e tests are updated to target the new
aria-label='Close' button and scope the close selector to the visible
modal wrapper.
* refactor(settings): migrate settings modals to AionModal standard variant
Migrate DirectorySelectionModal, AgentHubModal, CssThemeModal,
JsonImportModal and OneClickImportModal to AionModal variant='standard'
for the unified three-section layout, moving footer actions into the
footer render and dropping fixed heights so content sizes naturally.
No modal logic or field interactions are changed.
* fix(conversation): drain queued messages after switch
* fix(conversation): keep assistant streaming pinned
* feat(modal): let standard variant opt out of default body padding
Standard modals apply a default 20/24 body padding. Allow a caller to pass
contentStyle.padding (e.g. 0) to take over padding itself, so full-bleed
content such as the team-create two-column layout can sit flush to the
edges while still inheriting the standard header, footer, centering and
max-height rules. Additive and scoped to the standard variant — no effect
on modals that don't pass contentStyle.padding.
* refactor(team): migrate TeamCreateModal to AionModal standard variant
Adopt variant='standard' for the unified title/footer/centering/max-height
rules while keeping the team-specific two-column body full-bleed via
contentStyle padding:0. Move title+subtitle into the standard header and
the action buttons into the standard footer, dropping the hand-written
divider and padding. The close button is now button[aria-label=Close];
update the team e2e close selectors and the unit mock accordingly. No
modal logic or field interactions changed.
* test(e2e): isolate userData into a disposable sandbox
Add an E2E-only hook in configureChromium that redirects userData to an
explicit sandbox directory when AIONUI_E2E_TEST=1 and AIONUI_E2E_USER_DATA_DIR
is set, before any getPath() call. The fixture now creates a fresh temp
userData per run and cleans it up on exit.
Previously E2E launched against the developer's real database; a shared DB
that fails migration left AionCore unable to boot, blanking the whole app.
A per-run sandbox migrates a clean DB so the app starts reliably. Guarded
by AIONUI_E2E_TEST so dev and production are unaffected.
* feat(team): add narrow-screen layout for the create-team modal
Below the 768px mobile breakpoint the create-team modal switches from the
desktop two-column layout to a single column: only the selected-members
list and team fields show inline, and the assistant picker becomes a
compact dropdown anchored to an 'Add member' button in the list header.
Picking an assistant closes the dropdown so the member immediately appears
in the list below — the 'added successfully' feedback the bottom-sheet
approach lacked. The dropdown reuses TeamAssistantPicker and the existing
select/leader logic; the desktop two-column layout is unchanged.
TeamMemberDraftList gains an optional headerAction slot so the mobile
header can host the add button without duplicating the members title.
Covers the flow with new unit and e2e tests.
* feat(ui): add AionInlineSearchInput for dropdown list search
Extract a lightweight search box (soft grey fill, borderless, 8px radius)
matching the style the home project picker used before the unified search
box landed. It reads better inside click-to-open dropdown lists than the
bordered AionSearchInput, which suits persistent search bars.
Adopt it in the three dropdown lists that carry a top search: the home
project picker, the mid-chat add-member popover, and the new-team mobile
add-member dropdown. TeamAssistantPicker gains a searchVariant prop so
only the dropdown call-sites switch to the soft style; its persistent
desktop pane keeps the bordered AionSearchInput.
* fix(team): scroll the member list on mobile and pad the assistant dropdown
On narrow screens the selected-members list had no bounded height, so a
long list overflowed the modal without scrolling and pushed the team-name
and workspace fields off screen. Give the list box a fixed max-height on
mobile (via a new optional listBoxClassName on TeamMemberDraftList) so it
scrolls internally like the desktop pane while the fields stay visible.
Also increase the assistant dropdown's inner padding so its search box no
longer sits flush against the top border.
* fix(team): render blocked-assistant tooltip above the picker dropdown
The block-reason tooltip mounted inside the modal's stacking context, so
its z-index could not beat the add-member dropdown that is portaled to
document.body — the hint rendered behind the very list it belonged to.
Portal the tooltip to document.body as well and raise its z-index above
the dropdown so the reason is always visible on hover.
* refactor(team): extract shared assistant-picker dropdown with header
Both add-member entry points — the desktop team tab-bar '+' and the
new-team modal's mobile add button — opened a click-to-show dropdown that
wraps the assistant picker. Extract TeamAssistantPickerDropdown to own the
popup's positioning, z-index, and styling so the two sites share one
implementation and only supply the anchor and select handler.
Give the dropdown an optional title/subtitle header. The team tab-bar
dropdown now shows an 'Add member' title with a short 'The same assistant
can be added repeatedly' subtitle, replacing the longer footer hint. Add
the subtitle i18n key across locales and drop the now-unused footerHint.
* docs(team): add team runtime experience PRD and technical design
Capture the team collaboration UX overhaul: member identity colors,
capsule member bar, parallel/single view toggle, warmup loading state,
add-member/tell-leader guidance, and leader greeting — with the
front-end technical design (state ownership, color allocation, warmup
gating) and staged rollout plan.
* feat(team): add member identity color system
Assign each team member instance (slot_id) a low-saturation identity
color from a fixed palette: the leader is pinned to the brand color,
new members take the smallest free slot (reusing colors freed by
removals), and colors persist per team in localStorage so a member's
color never changes when others are added or removed. Palette cycles
when members exceed its size.
Expose colorOf(slot_id) and colorOfConversation(conversation_id) through
TeamTabsContext (the latter maps a teammate message's senderConversationId
back to its member), plus a TeamIdentityContext so the deep message
render chain can read colors without prop drilling. Pure front-end, no
backend or data-model changes.
* feat(team): capsule member bar with identity-colored names and selection
Rework the top member tabs into capsules: round avatar with a
bottom-right status dot, identity-colored member name, and a neutral
background (light default, one shade darker on hover/selected) — the
identity color lives on the name text, not the capsule fill. Member
rename/remove actions reveal on hover; the add-member entry is fixed at
the far right (borderless, with a left divider) and no longer scrolls
with the list.
Chat columns carry a faint identity tint with an identity-colored header
name; the column body stays light for message readability and only the
header deepens slightly when selected. Teammate message bubbles get an
identity-colored left bar and sender name. Add a gray divider between
columns.
The per-column remove button moves out of the panel header into the
capsule (single remove entry). Drop the now-obsolete content-panel
remove test (the removal + cron-cleanup ordering stays covered by
removeTeamAssistantWithCronCleanup.test.ts and the team-member-ops e2e).
* fix(team): refine capsule bar and header colors for readability
Keep member capsules on a constant light-grey fill (hover and selection
no longer darken it, which had washed out the colored name); show the
selected capsule with a border in the member's own identity color so it
reads clearly and echoes the name. Drop the identity tint from the column
header (it muddied the colored header name) and use a lighter grey header
background; the column body keeps only a faint identity tint. Remove the
per-column selection highlight (selection now lives on the capsule). Clean
up the add-member button: no native button border box and no grey block
in its disabled state.
* feat(team): add parallel/single collaboration view toggle
Add a view toggle in the title row (labeled 'View' + parallel/single
icons): parallel shows all member columns side by side, single fills the
screen with the currently selected member. The mode is remembered per
team in localStorage (default parallel) and only shows for teams with
2+ members.
Both views share the active member (switching a tab in single view
swaps the full-screen member; a column's fullscreen button jumps to
single view for that member). Replaces the ad-hoc fullscreenSlotId state
with a persisted viewMode. Add team.view.* i18n across locales.
* feat(team): add warmup loading overlay gated on leader readiness
Show a frosted overlay over the chat area when entering a team while the
leader runtime is still starting: member avatars light up as they become
ready, with a 'waking up the team (N/M)' label and a brand-colored
progress bar. The overlay clears as soon as the leader is ready — not
waiting for every member — so a single member that fails to start never
blocks the whole team. A 20s timeout and a leader-failure state fall back
to an error view with a retry action instead of spinning forever.
The overlay covers only the chat region (title row stays usable, so view
switching still works); add/remove/rename and sending are gated during
warmup via the existing membershipMutationBusy overlap. Add team.warmup.*
i18n across locales.
* feat(team): leader greeting and tell-the-leader guidance
Give the leader's empty state a first-person greeting ('Hi, I'm the
Leader. I understand your goal and coordinate the team…') in place of the
old subtitle, keeping the three suggestion cards.
Add a guidance footer to the add-member dropdown: 'Not a good fit? You
can also ask the Leader to arrange it. Tell the Leader →'. Clicking it
switches to the leader tab and pre-fills the leader's send box with 'Help
me add a member good at ___ to the team' (not sent), so users discover
they can grow the team through the Leader, not only via the GUI. Add the
new i18n keys across locales.
* fix(team): drive warmup overlay off the team session, not runtime events
The warmup overlay almost never appeared: on team entry the backend does
not broadcast agentRuntimeStatusChanged (those fire only on add-member
rebuilds) and members' TeammateStatus is already 'idle', so the gate
resolved to ready immediately. conversation.ensureRuntime is also wrong
here — the backend rejects standalone runtime-ensure for team-owned
conversations (TEAM_RUNTIME_REQUIRED).
Gate the overlay on team.ensureSession (POST /api/teams/{id}/session)
instead — the call that actually starts the team runtime on entry and
resolves when it's ready / rejects on failure. The overlay now shows
during the real warmup and clears when the session is ready. Add a
useTeamWarmup unit test and the ensureSession mock to the cron dom test.
* fix(team): full-width tell-Leader divider and auto-scroll to active column
- Make the add-member dropdown's tell-Leader guidance a full-width top
divider + text instead of an inset that reads as a boxed footer.
- In parallel view, scroll the newly active column into view when the
active slot changes programmatically (e.g. clicking Tell Leader).
- Drop the dead per-header remove handler left over after the remove
action moved to the capsule bar.
* fix(team): drive warmup avatars off real per-member runtime signal
The warmup overlay previously lit every avatar and showed N/N instantly:
it keyed off the team member work status (all 'idle' on entry), not the
runtime warmup, so the '(ready/total)' counter was cosmetic and always full.
- useTeamWarmup now also consumes agentRuntimeStatusChanged and exposes a
per-slot runtime status map (pending/ready/failed). The overlay lights
avatars from that signal: dim → breathing 'waking up' (leader first, as
the backend rebuilds staggered) → lit. Session readiness (overlay dismiss
/ failure) still gates on the authoritative team-level ensureSession
promise, since success is emitted all-at-once, not per member.
- Drop the misleading '(ready/total)' progress line for an honest subtitle;
add a team-warmup-breathe keyframe for the pending state.
- Replace the fixed 20s timeout with a no-progress watchdog reset on each
runtime event, so large staggered teams don't false-timeout.
- Disable add/remove/rename member while warming up (PRD requires it; the
buttons live in the header which the overlay does not cover).
* feat(team): actionable warmup failure with per-member recovery
When warmup fails, the overlay now identifies the failing member and
guides recovery instead of offering a dead-end retry:
- The overlay no longer covers the column headers (starts below the 40px
header row), so each member's model selector stays clickable — switching
the model is the shared self-rescue path for both members and the lead.
- The failure card names the failing member and its error, and branches
the guidance: a teammate can switch its model or be removed from the
capsule bar; the lead (which cannot be removed) can only switch its model.
The failing member's avatar is marked in both the card and its capsule.
- Member add/remove/rename are disabled only while actively warming, not in
the failed state — previously the failed state kept them disabled, which
trapped the user (they could not remove the member blocking startup).
- useTeamWarmup now carries the per-member error alongside its status so the
card can surface the reason.
Verified live against teammate-fail and lead-fail teams on aioncore 0.1.44.
* refactor(team): tighten the warmup failure card
The card repeated the member name, role and 'failed to start' across a
title line and a reason line, and dumped the raw wrapped backend error.
- Collapse title + reason into one line that states role, name and status
once (e.g. 'Lead {name} failed to start'); use 'Leader' rather than a
translated role word in Chinese.
- Show the error in a compact danger-tinted box, and strip the backend
wrapper noise (rebuild UUID, repeated 'Invalid request/Bad request'
prefixes) down to the meaningful tail via simplifyWarmupError.
- Hints become action-only (switch model / remove then retry), dropping the
role word they used to repeat.
* style(team): apply oxfmt formatting
Run the repo formatter over the team-runtime work so it passes the
push gate's fmt-check. Whitespace/line-wrapping only, no logic change.
* fix(team): warm runtime before config changes
* feat(team): summarise multiple warmup failures in one card
The failure card only reported the first failed member. The backend can
report several failures at once (concurrent rebuild batch), so surface
all of them:
- When more than one member fails, the card title becomes a count and the
error area lists each failed member with its simplified reason (leader
tagged), capped in height and scrollable. A single failure keeps the
named-member layout.
- The recovery hint offers removal whenever any failed member is a
teammate, and falls back to model-switch-only when only the lead failed.
- Nudge the overlay top from 40 to 44px so the column header's bottom
border stays visible above the frosted layer.
Adds TeamWarmupOverlay.dom.test.tsx covering single / multi / lead-only
/ ready states.
* fix(ui): center icon-park icons and strengthen the view toggle
icon-park renders each icon as an inline span.i-icon, so the SVG sits on
the text baseline and drifts upward wherever it shares a flex row (the
view-toggle icons, the retry button icon, the capsule edit/remove icons,
and more). Make .i-icon an inline-flex centering box globally so icons sit
truly centered — one fix, everywhere.
Also make the team view toggle's selected segment fill with the brand
color and a white icon (was brand-on-white, too subtle to read as active).
* fix(ui): stop single-side borders drawing a full phantom box
UnoCSS's border-solid sets border-style:solid on all four sides, while
the single-side width utilities (border-l/border-t/…) only set width on
their own side. The other three sides then fall back to the CSS initial
border-width of medium (~1.5px) and, now being solid, become visible — so
'border-l border-solid' drew a full ~1.5px box instead of one 1px line.
This showed up as stray frames around the add-member button, the picker's
tell-Leader guidance footer, and (doubled between adjacent columns) the
chat column headers.
Add a Tailwind-Preflight-style border baseline to the global preflight:
every element defaults to border-width:0 + border-style:solid, so a
single-side utility draws exactly its one line and nothing else. Fixes all
such phantom frames app-wide, not just the team page.
* fix(skills): show empty state for sections with no search matches
When a search query filtered a skills section down to zero results, the section rendered an empty container instead of any feedback. Each section (custom, official, extension, auto-injected) now shows the dashed empty-state box with a 'no matching skills' message, matching the existing empty-state styling. Adds the noSearchResults i18n key across all locales.
* fix(guid): cap home model selector width to prevent mobile overflow
The home-screen model selector button had no width constraint, so a long model label (e.g. 'global.anthropic.claude-opus-4-8 · Default') grew the non-shrinking action group and pushed the attach (+) button out of the input row on narrow viewports. The label now truncates with an ellipsis (max 220px on desktop, 90px on mobile) and the button is allowed to shrink, keeping the attach, model, mode and send controls inside the input box.
* fix(model): clear default base URL for Dashscope Coding Plan
The Dashscope Coding Plan platform pre-filled a fixed coding endpoint in the add-model form's Base URL field. Users on this plan need to supply their own endpoint, so the platform now has an empty default and the field starts blank.
* fix(settings): align Agent tab header with other settings tabs
The Agent tab wrapped its content in an extra py-16px/px-16px layer, which conflicted with the shared page padding and the header's negative-margin bleed, leaving the Agent header slightly higher than the Model/Skills/Tools tabs and causing a visible jump when switching tabs. Removing the redundant padding aligns all tab headers to the same top offset.
* refactor(model): remove multi-API-key editor entry from add-model modal
Removes the pencil suffix on the API Key input that opened the multi-key editor dialog, and deletes the now-unused ApiKeyEditorModal. Multi-key rotation is still configured from the platform edit screen (as the inline hint states).
* fix(ui): make default border color transparent and restore team divider
Follow-up to the border baseline fix. Two regressions from it:
- Elements that set a border width/side without a color class fell back to
the CSS initial border-color (currentColor = text color), so once the
baseline forced border-style:solid they rendered as stray black lines
across many pages. Default the baseline border-color to transparent, so
an unspecified border stays invisible (as before) while explicit colors
(border-base etc.) still show.
- The team member bar drew its bottom divider only via the old phantom
border; with that gone the line between the capsule bar and the column
headers vanished. Add an explicit border-b to the bar.
Also nudge the warmup overlay top from 44 to 41px so the column header's
1px bottom border stays visible.
* fix(team): simplify the add-member disabled state to lighter text only
The disabled add-member button showed a gray fill, sat higher than the
capsule row, and read as a boxed control. Causes: it used undefined
--color-text-* tokens (empty at :root, so the muted color never applied),
and the native button's UA disabled styling (gray background + collapsed
height) bled through and won over the utility classes in the disabled
state.
Point the text at the real --text-disabled / --text-secondary tokens, and
force appearance:none + transparent background + a locked 32px height via
inline style so the UA disabled chrome cannot override it. Disabled now
differs from enabled only by lighter text — same position, size, no fill,
no border.
* style(errors): preserve original error via cause when re-throwing
Lint auto-fix: pass { cause: error } when wrapping and re-throwing in the
team create flow and the e2e renderer-mount fixture, so the original error
is retained in the chain. No behavior change.
* fix(team): gate capsule ops on warmup only, not the mutation-busy flag
Capsule add/remove/rename were disabled by membershipMutationBusy in
addition to warmup. That flag is driven by pending / session_injecting
runtime events, so removing a member (which makes the backend rebuild the
rest) re-raised it and re-locked the bar — the 'delete one, wait, delete
next' stutter, and it also re-locked after a failed warmup where ops are
supposed to be open.
Gate capsule ops on the warmup phase alone: locked only while warming,
fully open once warmup ends (ready or failed). The backend already
serializes add/remove/rename via a per-team mutex, so the frontend flag was
never a correctness guard — only a visual busy hint — and dropping it from
the gate is safe. The flag itself stays in the session/context, just no
longer feeds the capsule disable state.
* feat(team): hover affordance on capsule actions and confirm every removal
- The capsule edit/remove icons now get a rounded hover background and
color shift (were opacity-only), so they read as real buttons.
- Removing a member always asks for confirmation now (previously only when
the member was actively working); the active case keeps the stronger
'work will be interrupted' wording, and the confirm button is styled as a
danger action.
* feat(team): member greeting, identity-colored name, add-member prompt in empty state
- Non-leader members now show a greeting subtitle too ('Hi, I'm a team
member...'), matching the leader's.
- The name under the avatar renders in the member's identity color instead
of the default text color.
- Swap the leader's middle empty-state suggestion for the 'ask the Leader
to add a member good at ___' prompt, reusing the tell-Leader wording.
i18n: add emptyState.memberGreeting and emptyState.suggestions.add_member,
and reword removeAgent.confirmContent (+ confirmContentActive) across
locales.
* Drive team warmup from session events
* Use team session status for warmup
* Sync team warmup state with member controls
* fix(i18n): drop duplicate skillsHub.noSearchResults key
settings.json had two skillsHub.noSearchResults entries per locale — an
older one plus a newer translated one. JS parsing silently keeps the last,
so it slipped through local checks, but CI's strict check-json rejects
duplicate keys. Remove the older orphan and keep the fully-translated
later entry (matches the code's defaultValue). No behavior change.
* fix(team): retry dynamic member warmup in place
* refactor(team): align UI with slot work coordination
* fix(conversation): preserve manual queue mode in background
---------
Co-authored-by: zynx <>
Co-authored-by: Waili(瓦砾) <icey.liu1994@gmail.com>
Co-authored-by: szafranski <p.fabiszewski@gmail.com>
* feat(guid): move mobile home input controls into a + action sheet (#3554)
* feat(chat): add multi-select mode to MobileActionSheet
Add a `multiSelect` option to action-sheet submenus. Multi-select submenus
render checkboxes (driven by `option.active`) and, on tap, toggle the selection
via `onSelect` while keeping the submenu open so several items can be toggled in
a row — distinct from single-select (radio, slides back) and action rows
(no marker, closes the sheet). Used for Skills / MCP on the home page.
Backward compatible: existing submenus without `multiSelect` are unchanged.
Add focused tests for single-select, multi-select, and action-row behaviour.
* feat(guid): move mobile input controls into a + action sheet
On mobile, the home page input row previously laid model and permission
selectors out inline (with a hover-dropdown + for files/skills/MCP), which was
cramped and needed horizontal scrolling. Collapse everything into the + button:
tapping it opens the bottom action sheet with model / thought level (ACP only)
/ permission (single-select), add files, and skills / MCP (multi-select
checkboxes, showing a selected count on the main row).
Desktop is unchanged — it keeps the inline selectors and the hover dropdown.
GuidPage passes the flat model/thought data down so GuidActionRow can build the
sheet entries; switch callbacks are reused as-is.
* refactor(conversation): rename loaded skills/MCP to selected
In the conversation + menu (mobile action sheet and desktop dropdown), the
skills and MCP entries listed what is currently active, labelled "Skills" /
"Loaded MCP" / "Loaded Skills". Rename them to "Selected skills" / "Selected
MCP" so the wording reflects that these are the chosen items — distinct from the
home page, where the same controls are the picker itself ("Skills" / "MCP").
The conversation skills indicator keeps "Loaded skills" (it reports live state).
* chore(i18n): add selected skills/MCP and selected-count strings
Add common.selectedSkills, common.selectedCount ({{count}}) and
conversation.mcp.selected across all 12 locales for the home-page action sheet
and the renamed conversation skills/MCP entries. Regenerate i18n key types.
* feat(i18n): add French locale (#2731)
Co-authored-by: zk <zk@users.noreply.local>
* chore: bump version to 2.1.32 and aioncore to v0.1.45 (#3555)
Co-authored-by: zynx <>
* fix(build): verify current Codex ACP Windows binaries (#3557)
Co-authored-by: zynx <>
* fix(build): align Codex installer verifier (#3561)
- Check the OpenAI Codex platform binary layout currently bundled by AionCore on Windows
- Add a focused regression test so the stale Zed Codex ACP path is not reintroduced
* chore: bump version to 2.1.33 (#3562)
Co-authored-by: zynx <>
* fix(conversation): prevent queue drain from racing backend idle state (#3571)
Three related 409-CONFLICT scenarios were identified where the draft-box
queue fires a message before the backend has fully released the conversation:
1. After a turn completes naturally, the execution gate flips canExecute
based on an optimistic local runtime snapshot that may precede the
backend actually becoming idle.
2. After the user clicks Stop, the queue drain fires immediately once
handleStop resolves, but the backend may still be processing the abort.
3. "Send Now" called sendNow() directly after effectiveHandleStop(),
bypassing the execution gate entirely.
Additionally, when onExecute was called before the state update settled,
a message could disappear from the draft box without being confirmed sent
(remove and restore mutate calls raced each other).
Fixes:
- Add prioritize() to useConversationCommandQueue: moves a command to the
front of the queue and ensures mode=auto, letting the drain effect send
it through the normal gate rather than bypassing it.
- Replace sendNow() in handleSendNowQueued (both AcpSendBox and
AionrsSendBox) with effectiveHandleStop() + prioritize(), so "Send Now"
honours the execution gate and waits for the backend to confirm idle.
- In the foreground drain effect, await updateState before calling
onExecute so the item only leaves the UI once the send is accepted (or
is restored on failure), eliminating the remove/restore mutate race.
- On 409 CONFLICT (isConversationBusyError) in both the foreground drain
catch and drainBackgroundCommandQueue, restore the command without
pausing the queue and re-trigger the effect (via setExecutionGateVersion
or a short setTimeout retry) instead of surfacing an error to the user.
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
* fix(team): show accepted team work as processing (#3576)
Co-authored-by: zynx <>
* chore: bump version to 2.1.34 and aioncore to v0.1.46 (#3578)
Co-authored-by: zynx <>
* fix(startup): classify assistant bootstrap failures (#3583)
Co-authored-by: zk <zk@users.noreply.local>
* fix(feedback): attach team route context
* test(feedback): cover blank route identifiers
* chore(docs): update WeChat group QR code to wx-16 (#3588)
- Replace wx-15.png with wx-16.png across all README files
- Update references in main README and localized versions
- Remove outdated wx-15.png image
* fix(packaging): verify bundled resources from manifest (#3587)
* fix(packaging): verify bundled resources from contract
Consume AionCore managed-resources/manifest.json in packaging and Windows install verification. Reject missing, invalid, duplicate, or old-version-only managed Node and ACP resources, and update the legacy ACP artifact script to current package pins.
* test(build): strengthen managed resource verifier gates
Reject non-numeric managed resources schemaVersion values in the Windows installer verifier and add focused prepare-aioncore gate coverage for Actions artifacts, GitHub release downloads, and local binary fallback inputs.
* test(packaging): skip POSIX fake aioncore gates on Windows
* chore(packaging): bump bundled AionCore to v0.1.47
---------
Co-authored-by: zynx <>
* fix(conversation): handle busy send conflicts (#3589)
* fix(conversation): handle busy send conflicts
Classify AionCore 409 conversation busy responses as non-retryable conversation busy errors, preserve the runtime send gate for busy failures, and suppress internal error cards in ACP, initial ACP, and Aionrs send paths.\n\nRestore queued commands on busy conflicts and wait for the runtime gate to observe a blocked state before draining again, while keeping foreground and background queue runners from racing after execute callback identity changes.\n\nAdd focused regression coverage for busy classification, runtime gate preservation, command queue restore/wait, ACP/Aionrs send paths, initial message handling, and team safety.
* fix(conversation): preserve observed busy queue gate
Keep the foreground queue busy-wait latch aware of a blocked runtime gate that was already observed before a 409 busy rejection reaches the queue catch path. This prevents restored queued commands from getting stuck after the gate later becomes sendable.
Add a regression test for the pre-observed blocked-gate timing.
---------
Co-authored-by: zynx <>
* fix(renderer): restrict message file marker parsing (#3590)
Preserve literal [[AION_FILES]] text in assistant, left-side, and teammate messages. Only parse right-side user attachment blocks when the marker is standalone and followed by local-looking file paths.
Co-authored-by: zynx <>
* chore: bump version to 2.1.35 (#3591)
Co-authored-by: zynx <>
* feat(cron): add queue protection and custom schedules (#3552)
Co-authored-by: zk <zk@users.noreply.local>
* chore(deps): remove aioncli-core dependency (#3594)
Co-authored-by: zk <zk@users.noreply.local>
* chore(deps): remove office-ai platform dependency (#3595)
Co-authored-by: zk <zk@users.noreply.local>
* feat(skills): add batch delete for custom skills (#3600)
* feat(skills): add batch delete for custom skills
Add a batch-manage mode to the Custom skills tab: multi-select via
checkboxes or card click, select-all over the filtered list, and a
single confirmation to delete the selected skills. Reuses the existing
single-delete REST endpoint per skill (no backend changes); partial
failures surface a warning with success/failure counts. Batch mode
resets on tab switch. Adds i18n keys across all 13 locales and unit
coverage for enter/select/delete/partial-failure/cancel flows.
* fix(settings): add top spacing to mobile settings nav
The horizontal pill nav on mobile settings pages sat flush against the
title bar. Add 14px top padding so it no longer looks clipped.
* test(e2e): cover skills batch delete and repair stale skills helpers
Add TC-S-05b covering the batch delete flow end-to-end (enter batch
mode, select two of three skills, confirm, verify backend state).
Repair two stale bits in the skills E2E helpers that broke the whole
suite's navigation and setup:
- goToSkillsHub still targeted /settings/capabilities, which no longer
exists since skills/tools were split into standalone entries; point
it at /settings/skills.
- importSkillViaBridge sent camelCase skillPath while the backend
expects snake_case skill_path, so every bridge import silently
failed with 400.
* style(skills): soften batch-mode selected card state (#3603)
Replace the primary border + tinted background on selected cards with
a borderless light fill, so selection reads from the checkbox and
background alone without a heavy outline.
* feat(skills): skill detail page with assistant attachment (#3604)
* refactor(skills): group skills settings files into SkillsSettings directory
pages/settings/ exceeded the 10-child directory limit; move
SkillsHubSettings.tsx and skillImportMessages.ts into a SkillsSettings/
module directory ahead of adding the skill detail page.
* feat(skills): add skill detail page with assistant attachment
New /settings/skills/detail/:skillName route styled after the assistant
editor page. A single used-by list is the source of truth for the
skill-assistant relation: an 'Attach to assistant' dropdown lists only
assistants not yet using the skill, each row exposes an inline remove
and links to the assistant editor. Attach/detach reuses the existing
PUT /api/assistants/:id partial update (enabled_skills only) — no
backend changes. Builtin assistants are listed read-only since their
update path only accepts agent/defaults fields. Adds i18n keys across
all 13 locales and unit coverage for add-menu filtering, attach,
inline detach, and builtin read-only handling.
* feat(skills): link list cards to detail page and show used-by avatars
Skill cards (custom and official) navigate to the new detail page on
click; in batch mode the card click still toggles selection. Cards with
attached assistants show a compact overlapping avatar stack (+N
overflow, tooltip with names), mirroring the agent page's bound-
assistant stack. Per-card delete stops propagation so it no longer
triggers navigation.
* feat(ui): add search to skills/MCP submenus and assistant default selects (#3605)
* refactor(agent): generalize model-list search threshold and scroll class for reuse
* feat(guid): add search to skills and MCP submenus in the plus menu
* feat(assistants): make default model/skills/MCP selects searchable
* test(e2e): cover dropdown search in plus menu and assistant editor
* feat(ui): standardize drag-to-reorder UX for team tabs and pinned conversations (#3606)
* feat(conversation): drag pinned conversations via hover handle with in-place drop
* feat(team): rewrite member tab reordering with dnd-kit sortable
* refactor(chat): reuse shared vertical-axis drag modifier in command queue
* chore(i18n): regenerate i18n key types
* fix(build): resolve app-builder-lib within the current repo when patching NSIS template
* chore: bump version to 2.1.36 and aioncore to v0.1.48 (#3610)
Co-authored-by: zynx <>
* fix(bridge): accept void-param invokes after JSON serialization (#3611)
* fix(bridge): accept void-param invokes after JSON serialization
Zero-argument bridge invokes (window-controls:minimize/maximize/close,
autoUpdate.checkForUpdates, application.restart, etc.) send
`data: undefined`, which JSON.stringify drops from the wire payload on
Electron IPC and WebSocket transports. The subscribe guard introduced in
#3595 required the `data` key to be present, so every zero-arg request
was silently discarded — on Windows this broke the custom titlebar
minimize/maximize/close buttons in v2.1.36 (Sentry ELECTRON-3JZ).
Drop the `'data' in request` requirement and read the property
optionally; add a unit test that round-trips the request through JSON
like the real transports do.
* test(e2e): cover window-controls zero-arg invokes over real IPC
Regression coverage for the v2.1.36 bridge bug: drive
window-controls:is-maximized/maximize/unmaximize through the real
Electron IPC transport and assert the BrowserWindow state changes from
the main process. Both tests fail on the pre-fix bridge guard.
* fix(renderer): keep team elapsed timer continuous across remount (#3612)
The team (assistant-preset) member-column processing timer in
ThoughtDisplay was implemented as a local counter seeded from
Date.now() on mount and reset on every timing useEffect re-run. Any
subtree remount or dependency churn (opening settings and returning,
toggling fullscreen/windowed, resizing the window) reinitialized the
origin to "now", so the elapsed time restarted from 0s
(Sentry ELECTRON-3J5 / 134761572).
Drive the elapsed time from the backend-provided absolute start
timestamp (ITeamSlotWork.active_turn_started_at_ms) along the team
chain so it survives remounts:
- ThoughtDisplay: add startedAtMs and externalElapsedSource props and a
three-state timer. State A (external mode + valid timestamp) computes
elapsed as now - startedAtMs, recomputing from the same origin after
remount. State B (external mode + invalid timestamp) suppresses the
number and shows only status text and spinner. State C (non-external)
keeps the existing local useRef(Date.now()) behavior, so non-team
sessions are unchanged.
- teamSendRuntime: expose startedAtMs from work.active_turn_started_at_ms
(null-normalized) on TeamSendBoxRuntime.
- AcpSendBox / AionrsSendBox: forward externalElapsedSource and
startedAtMs to ThoughtDisplay, gated on the presence of teamRuntime.
Add focused Vitest coverage for the new behavior (external timestamp,
remount continuity, per-second advance, invalid-timestamp suppression,
new-turn recompute, non-team local fallback, and runtime timestamp
propagation). No backend, schema, or i18n changes.
Co-authored-by: zynx <>
* chore: bump version to 2.1.37 (#3613)
Co-authored-by: zynx <>
* feat(settings): add agent and assistant search (#3616)
* feat(settings): add agent and assistant search
* feat(settings): add scheduled task search
* fix(settings): prevent scheduled tasks header bleed-through
* fix(settings): keep scheduled tasks scrolling local
* fix(settings): isolate scheduled task header scroll
---------
Co-authored-by: zk <zk@users.noreply.local>
* fix(settings): hide agent search on mobile (#3617)
Co-authored-by: zk <zk@users.noreply.local>
* fix(team): treat idle-stopped session as recoverable, not a draft-box block (#3618)
After an idle-clean reclaim, sending a team message enqueued it to the draft
box and never drained, because `session_stopped` was classified as a fatal
block (`canSendMessage=false` → `isBusy=true` → `enqueue()`), and the only
recovery call lives in the skipped `executeCommand` path. The stopped state
was also unreliable, derived from a stale slot value.
Make the stopped state recoverable-and-sendable and drive it from a reliable
event, so the existing lazy-recovery send path fires.
- types: add `'stopped'` to the `TeamSessionStatus` union.
- useTeamRunView: add an event-driven `sessionStopped` flag set by
`sessionStatusChanged('stopped')`, cleared on `starting`/`ready` and on any
applied active run event (self-heal), independent of reconciled slot work.
- teamSendRuntime: drop `session_stopped` from `FATAL_BLOCK_REASONS` (a stale
stopped slot no longer blocks sending, still shows stopped text); add a
`sessionStopped` option that forces `canSendMessage=true` and `loading=false`.
- TeamChatView: wire the flag through and prefer the stopped prompt so it shows
even when the stale slot has no blocked_reason.
- useTeamWarmup / teamMembershipMutationBusy: handle `'stopped'` explicitly
(keep warmup phase; treat as non-mutation-busy).
- tests: cover the recoverable-stopped runtime, the event-driven flag and its
self-heal, membership non-busy handling, and update the send-box mapping test
for the intentional `session_stopped` behavior change.
Co-authored-by: zynx <>
* fix(installer): run arch check before registry mutation (#3619)
- E1040 arch guard lived in .onVerifyInstDir, which fires after customInit
has already healed/cleared/repaired an existing install's registry; an
arm64 build run on an x64 machine thus mutated the correct-arch install's
registry and uninstaller before aborting
- Move the guard into preInit (after session-begin, before active-marker
writes and customInit) so a wrong-arch installer exits without touching
any registry or uninstaller state
- Report the real detected arch (arm64/x64/x86 via IsWow64Process2) instead
of the hardcoded actual=non-arm64, so triage can tell x64 from x86
- x64/arm64 rejection policy is unchanged; only timing and diagnostics change
Refs Sentry ELECTRON-3BX
* fix(system): let backend own keep-awake blocker (#3620)
Co-authored-by: zk <zk@users.noreply.local>
* feat(guid): expand assistant more dropdown into responsive multi-column panel (#3621)
* feat(guid): expand assistant more dropdown into responsive multi-column panel
* feat(guid): open assistant overflow panel on pill bar hover
* feat(guid): add subtle entry animation to assistant overflow panel
* feat(guid): hide overflow search until assistant list exceeds five rows
* feat(guid): task-oriented default prompts with refined suggestion styling (#3622)
* feat(guid): replace default assistant prompts with task-oriented suggestions
* style(guid): restyle prompt suggestions as plain text with hover arrow
* style(chat): use corner-right-up icon for send buttons
* chore: bump version to 2.1.38 and aioncore to v0.1.49 (#3625)
Co-authored-by: zynx <>
* fix(settings): keep agent repair panel mounted during background revalidation (#3624)
* chore(docs): update WeChat group QR code to wx-17 (#3623)
* chore(docs): update WeChat group QR code to wx-17
* fix(docs): rebuild wx-17 with proportional resize (no distortion)
* fix(docs): tighten wx-17 crop so QR fills frame like the right side
* fix(chat): restore arrow-up icon on send buttons (#3627)
* feat(feedback): route-aware module preselection and ask-the-butler chip on error surfaces (#3626)
* fix(feedback): map every route to its feedback module for the titlebar report button
The titlebar report button only knew 3 routes (conversation/team/settings),
so scheduled tasks, assistants and every settings tab fell through to no
module or a blanket system-settings. Extract the mapping into
resolveFeedbackModule with full route coverage; a unit test walks Router.tsx
so future pages cannot silently miss a module.
* feat(feedback): add ask-the-butler chip to conversation error surfaces
Every conversation error (structured tips, plain/JSON error tips, tool
failures, agent status errors) now shows a Butler chip next to the report
pill. Clicking routes to the home chat with the Butler selected and a
diagnosis prompt containing the error text pre-filled — same flow as the
report modal's solve-via-chat action.
The Butler chip shows on all errors, including user-environment ones that
opt out of reporting via feedback_recommended=false: those are exactly what
the Butler diagnoses best, while the report pill stays hidden for them.
* test(e2e): extend ACP stream injector with error fabrication and cover butler chip
Add emitErrorTip/emitToolError/emitAgentStatusError to the E2E stream
injector so tests can fabricate the three conversation error surfaces
without a real broken agent, and add a spec that walks the butler chip
end to end (error bubble -> home chat with diagnosis prompt seeded).
* test(e2e): repair drifted feedback specs
Four cases were failing on main: the About row label changed
(bugReport i18n copy), the custom-agent editor entry became a
TalkToButlerButton dropdown, and scenarios 5/6 asserted a feedback pill
that was deliberately removed from InlineAgentEditor in #3448. Update
selectors and expectations, and scope modal lookups to the visible
instance since multiple FeedbackReportModal mounts coexist.
* feat(settings): promote Kimi/Moonshot placement in platform and agent lists (#3629)
* feat(settings): move Moonshot platforms to top of model platform list
* feat(settings): pin Kimi after the builtin agent in the Agents list
* feat(settings): default add-model platform to the first list entry
* fix(preview): render distinct heading texts in markdown preview (#3630)
Streamdown's built-in heading components are memoized by node position
only and ignore children, so headings kept stale text when positions
were dropped (rehype-raw) or identical across re-renders. Override
h1-h6 with plain components that keep the built-in classes but always
render the current text.
* feat(github): automated issue/PR/discussion triage to module owners (#3631)
* feat(github): add module dropdown to bug and question issue templates
* feat(github): auto-label and auto-assign issues by module via triage workflow
* feat(github): add CODEOWNERS for module-based PR review routing
* feat(github): welcome-comment and mention duty owner on new discussions
* style(github): apply oxfmt formatting to triage map
* fix(github): use English-only module dropdown with exact-match triage parsing (#3636)
* docs(readme): add AionUi × Kimi contributor campaign section (#3643)
* docs(readme): add AionUi × Kimi contributor campaign section
* docs(readme): simplify campaign CTA and relocate Kimi links to platforms section
* docs(readme): inline campaign CTA and move Kimi links to K3 promo line
* docs(readme): emphasize campaign CTA, de-emphasize Kimi promo links
* docs(readme): banner as sole headline, single CTA, footnote promo
* docs(readme): restore Chinese caption as translation for English banner
* docs(readme): restore English campaign caption for SEO/bot indexing
* docs(readme): full campaign section for all 8 remaining locales
* docs(readme): link campaign CTA to X announcement and claim thread
* fix(agent-settings): hide launch path for npx agents and fix repair-panel status banner (#3641)
Two issues in the agent repair panel (AgentRepairPanel):
1. resolveDiagnosticBanner mapped any status other than missing/offline to the
"Connected" success banner, so an `unchecked` agent showed "Connected" in
the panel while the list correctly showed "Not checked". Handle `online`
explicitly and show a neutral info banner for `unchecked` that points the
user to Test Connection (top-right). While unchecked, the panel now shows
only that banner — launch path, env vars and Save & Test are hidden until a
check has run.
2. The launch-path override was offered for npx-bridged agents. Overriding it
forwards the npx wrapper args to the resolved binary and breaks startup, so
it is now hidden for bridge-launched rows (bridge_binary != binary_name),
matching the backend rejection.
Adds settings.repair.uncheckedTitle/uncheckedHint across all 13 locales and
regenerates i18n key types.
Co-authored-by: zk <zk@users.noreply.local>
* fix(workspace): stable file tree — expand state, search, preview panel (#3642)
* fix(workspace): restore pre-search expanded state on search clear
Searching a broad term opens dozens of directories; clearing the box
previously called refreshWorkspace() with all those dirs still in
expandedKeys, causing a burst of parallel IPC fetches and visible jank.
Now useWorkspaceSearch snapshots expandedKeys just before the first
search keystroke and restores it when the box is cleared, so the
refresh only refetches the small set of dirs that were open originally.
* feat(workspace): incremental tree refresh and per-project expansion cache
applyFreshListings: refresh re-fetches root + all expanded dirs in parallel
and splices fresh content into the existing tree. Expanded dirs stay open and
newly created files appear immediately — no more collapse-on-refresh and no
more stale children blocking new entries.
workspaceTreeCache: module-level LRU (20 entries) keyed by workspace path.
The tree and expandedKeys snapshot survives React remounts triggered by SWR
revalidation, so switching conversations within the same project restores the
exact tree state without a visible flash or reset.
Also includes buildSearchTree (frontend recursive file-name search at any
depth) and collectExpandedDirs (used by the refresh logic to identify dirs
that need a re-fetch).
* refactor(workspace): rewrite tree and events hooks for stable cross-conversation state
useWorkspaceTree: hydrates initial state from workspaceTreeCache on mount;
writes the cache snapshot on unmount and after every successful refresh (not
on every expand/collapse, which caused jank in large workspaces); detects
workspace path changes without remount and re-seeds from the cache.
useWorkspaceEvents: now tracks workspace path instead of conversation_id.
Same-workspace conversation switches only clear selection and call
refreshWorkspace; different-workspace switches also bump treeKey to force
re-render. Removes the backend search provider listener (search moved to
frontend).
* feat(preview): keep file preview open across same-workspace conversation switches
PreviewContext gains closePreviewIfWorkspaceChanged, backed by a stable ref
(lastWorkspaceRef) that survives React remounts. Switching to a conversation
in the same project keeps the preview panel open; switching to a different
workspace or project closes it.
Layout.tsx closes the preview unconditionally when the user navigates away
from /conversation/ routes (to team, settings, etc.), bypassing the workspace
comparison which would no-op when lastWorkspaceRef was never set on those
routes.
* perf(workspace): eliminate per-node O(N) expand lookup and inline renderer churn
expandedKeys.includes() inside renderTitle did a linear scan of the
expandedKeys array for every visible node on every render; with 100+
expanded dirs this meant tens-of-thousands of iterations per
expand/collapse, causing visible jank.
Fix: derive expandedKeysSet (Set<string>) from expandedKeys via useMemo
so FileTypeIcon gets an O(1) .has() check instead.
Also memoize treeData (flattenSingleRoot), treeIcons, and renderTitle
with useMemo/useCallback so Arco Tree does not rebuild all node
renderers on every expandedKeys state update.
* revert: perf(workspace): eliminate per-node O(N) expand lookup and inline renderer churn
Reverts commit 46984d606. The memoization changes broke existing
interaction behaviors; reverting to restore correct functionality.
* fix(preview): correct cross-workspace close behavior for teams and conversations
Bug 1 — preview not closing between teams with different workspaces:
TeamPageContent now calls closePreviewIfWorkspaceChanged(effectiveWorkspace)
whenever its workspace changes, mirroring what conversation/index.tsx does.
lastWorkspaceRef in PreviewContext is updated on every team navigation, so
switching from team A (/foo) to team B (/bar) correctly triggers a close.
Bug 2 — preview incorrectly closing when switching between conversations
of the same workspace:
Sider.handleConversationSelect was unconditionally calling closePreview()
on every conversation click. Removed that call. The workspace-aware
closePreviewIfWorkspaceChanged in conversation/index.tsx is the sole
decision point: same workspace → keep open, different workspace → close.
* refactor(workspace): remove dead mergeLoadedChildren code
applyFreshListings supersedes mergeLoadedChildren; no callers remain.
* feat(workspace): restore multi-tab file preview
Clicking a second file now opens a new preview tab instead of
replacing the first, matching the previous behavior before the
workspace refactor.
* fix(workspace): persist lazy-loaded children and fix minor memory leaks
- loadMore now writes the updated tree to workspaceTreeCache so lazy-
expanded subdirs survive a subsequent refresh or cache rehydration
- flattenSingleRoot is now memoized (useMemo) to avoid a redundant
full-tree traversal on every expand/collapse re-render
- setLoadingHandler stores the debounce timer in a ref and clears it
on unmount, preventing setState-on-unmounted-component warnings
* test: add PreviewContext mock to Layout and TeamPage DOM tests
Layout.tsx and TeamPage.tsx now call usePreviewContext; mock it in
unit tests that render these components without a PreviewProvider.
* fix(conversation): show sign-in hint for empty ACP turns needing auth (#3644)
Adds the ACP_EMPTY_TURN_NEEDS_AUTH agent-tip copy (with a {{hint}} param for the
agent's advertised login instruction) across all 13 locales, and regenerates the
i18n key types. Renders the actionable hint the backend now emits when an agent
ends a turn empty because it isn't signed in, replacing the blank "no visible
reply" tip.
Co-authored-by: zk <zk@users.noreply.local>
* feat(settings): configure model capabilities (#3639)
* feat(settings): configure model capabilities
* fix(settings): preserve automatic vision detection
* fix(settings): use dropdowns for model capability choices
* test(settings): cover model capability configuration
* test(settings): cover capability fallback branches
* fix(github): never auto-assign bonus-labeled issues in triage workflow (#3647)
* feat(github): post claim invitation when an issue is labeled bonus (#3649)
* fix(startup): stop false "local data repair failed" alarm from concurrent startup (#3650)
A second AionUi instance that lost the single-instance lock still registered
the backend startup flow, spawning a competing aioncore over the same data
directory. The losing bootstrap of assistant storage then failed transiently
and was unconditionally misclassified as fatal local data corruption, showing
users a panic-inducing "reinstall may not fix this" dialog (Sentry 135525166).
Layer 1 — single-instance gating:
- Extract shouldRegisterBackendStartup(gotTheLock) into a testable module and
gate the whenReady -> handleAppReady backend startup registration on it, so a
lock-losing instance never spawns a comp…
acetaxxxx
added a commit
to acetaxxxx/AionUi
that referenced
this pull request
Aug 28, 2026
* chore(deps): remove office-ai platform dependency (#3595)
Co-authored-by: zk <zk@users.noreply.local>
* feat(skills): add batch delete for custom skills (#3600)
* feat(skills): add batch delete for custom skills
Add a batch-manage mode to the Custom skills tab: multi-select via
checkboxes or card click, select-all over the filtered list, and a
single confirmation to delete the selected skills. Reuses the existing
single-delete REST endpoint per skill (no backend changes); partial
failures surface a warning with success/failure counts. Batch mode
resets on tab switch. Adds i18n keys across all 13 locales and unit
coverage for enter/select/delete/partial-failure/cancel flows.
* fix(settings): add top spacing to mobile settings nav
The horizontal pill nav on mobile settings pages sat flush against the
title bar. Add 14px top padding so it no longer looks clipped.
* test(e2e): cover skills batch delete and repair stale skills helpers
Add TC-S-05b covering the batch delete flow end-to-end (enter batch
mode, select two of three skills, confirm, verify backend state).
Repair two stale bits in the skills E2E helpers that broke the whole
suite's navigation and setup:
- goToSkillsHub still targeted /settings/capabilities, which no longer
exists since skills/tools were split into standalone entries; point
it at /settings/skills.
- importSkillViaBridge sent camelCase skillPath while the backend
expects snake_case skill_path, so every bridge import silently
failed with 400.
* style(skills): soften batch-mode selected card state (#3603)
Replace the primary border + tinted background on selected cards with
a borderless light fill, so selection reads from the checkbox and
background alone without a heavy outline.
* feat(skills): skill detail page with assistant attachment (#3604)
* refactor(skills): group skills settings files into SkillsSettings directory
pages/settings/ exceeded the 10-child directory limit; move
SkillsHubSettings.tsx and skillImportMessages.ts into a SkillsSettings/
module directory ahead of adding the skill detail page.
* feat(skills): add skill detail page with assistant attachment
New /settings/skills/detail/:skillName route styled after the assistant
editor page. A single used-by list is the source of truth for the
skill-assistant relation: an 'Attach to assistant' dropdown lists only
assistants not yet using the skill, each row exposes an inline remove
and links to the assistant editor. Attach/detach reuses the existing
PUT /api/assistants/:id partial update (enabled_skills only) — no
backend changes. Builtin assistants are listed read-only since their
update path only accepts agent/defaults fields. Adds i18n keys across
all 13 locales and unit coverage for add-menu filtering, attach,
inline detach, and builtin read-only handling.
* feat(skills): link list cards to detail page and show used-by avatars
Skill cards (custom and official) navigate to the new detail page on
click; in batch mode the card click still toggles selection. Cards with
attached assistants show a compact overlapping avatar stack (+N
overflow, tooltip with names), mirroring the agent page's bound-
assistant stack. Per-card delete stops propagation so it no longer
triggers navigation.
* feat(ui): add search to skills/MCP submenus and assistant default selects (#3605)
* refactor(agent): generalize model-list search threshold and scroll class for reuse
* feat(guid): add search to skills and MCP submenus in the plus menu
* feat(assistants): make default model/skills/MCP selects searchable
* test(e2e): cover dropdown search in plus menu and assistant editor
* feat(ui): standardize drag-to-reorder UX for team tabs and pinned conversations (#3606)
* feat(conversation): drag pinned conversations via hover handle with in-place drop
* feat(team): rewrite member tab reordering with dnd-kit sortable
* refactor(chat): reuse shared vertical-axis drag modifier in command queue
* chore(i18n): regenerate i18n key types
* fix(build): resolve app-builder-lib within the current repo when patching NSIS template
* chore: bump version to 2.1.36 and aioncore to v0.1.48 (#3610)
Co-authored-by: zynx <>
* fix(bridge): accept void-param invokes after JSON serialization (#3611)
* fix(bridge): accept void-param invokes after JSON serialization
Zero-argument bridge invokes (window-controls:minimize/maximize/close,
autoUpdate.checkForUpdates, application.restart, etc.) send
`data: undefined`, which JSON.stringify drops from the wire payload on
Electron IPC and WebSocket transports. The subscribe guard introduced in
#3595 required the `data` key to be present, so every zero-arg request
was silently discarded — on Windows this broke the custom titlebar
minimize/maximize/close buttons in v2.1.36 (Sentry ELECTRON-3JZ).
Drop the `'data' in request` requirement and read the property
optionally; add a unit test that round-trips the request through JSON
like the real transports do.
* test(e2e): cover window-controls zero-arg invokes over real IPC
Regression coverage for the v2.1.36 bridge bug: drive
window-controls:is-maximized/maximize/unmaximize through the real
Electron IPC transport and assert the BrowserWindow state changes from
the main process. Both tests fail on the pre-fix bridge guard.
* fix(renderer): keep team elapsed timer continuous across remount (#3612)
The team (assistant-preset) member-column processing timer in
ThoughtDisplay was implemented as a local counter seeded from
Date.now() on mount and reset on every timing useEffect re-run. Any
subtree remount or dependency churn (opening settings and returning,
toggling fullscreen/windowed, resizing the window) reinitialized the
origin to "now", so the elapsed time restarted from 0s
(Sentry ELECTRON-3J5 / 134761572).
Drive the elapsed time from the backend-provided absolute start
timestamp (ITeamSlotWork.active_turn_started_at_ms) along the team
chain so it survives remounts:
- ThoughtDisplay: add startedAtMs and externalElapsedSource props and a
three-state timer. State A (external mode + valid timestamp) computes
elapsed as now - startedAtMs, recomputing from the same origin after
remount. State B (external mode + invalid timestamp) suppresses the
number and shows only status text and spinner. State C (non-external)
keeps the existing local useRef(Date.now()) behavior, so non-team
sessions are unchanged.
- teamSendRuntime: expose startedAtMs from work.active_turn_started_at_ms
(null-normalized) on TeamSendBoxRuntime.
- AcpSendBox / AionrsSendBox: forward externalElapsedSource and
startedAtMs to ThoughtDisplay, gated on the presence of teamRuntime.
Add focused Vitest coverage for the new behavior (external timestamp,
remount continuity, per-second advance, invalid-timestamp suppression,
new-turn recompute, non-team local fallback, and runtime timestamp
propagation). No backend, schema, or i18n changes.
Co-authored-by: zynx <>
* chore: bump version to 2.1.37 (#3613)
Co-authored-by: zynx <>
* feat(settings): add agent and assistant search (#3616)
* feat(settings): add agent and assistant search
* feat(settings): add scheduled task search
* fix(settings): prevent scheduled tasks header bleed-through
* fix(settings): keep scheduled tasks scrolling local
* fix(settings): isolate scheduled task header scroll
---------
Co-authored-by: zk <zk@users.noreply.local>
* fix(settings): hide agent search on mobile (#3617)
Co-authored-by: zk <zk@users.noreply.local>
* fix(team): treat idle-stopped session as recoverable, not a draft-box block (#3618)
After an idle-clean reclaim, sending a team message enqueued it to the draft
box and never drained, because `session_stopped` was classified as a fatal
block (`canSendMessage=false` → `isBusy=true` → `enqueue()`), and the only
recovery call lives in the skipped `executeCommand` path. The stopped state
was also unreliable, derived from a stale slot value.
Make the stopped state recoverable-and-sendable and drive it from a reliable
event, so the existing lazy-recovery send path fires.
- types: add `'stopped'` to the `TeamSessionStatus` union.
- useTeamRunView: add an event-driven `sessionStopped` flag set by
`sessionStatusChanged('stopped')`, cleared on `starting`/`ready` and on any
applied active run event (self-heal), independent of reconciled slot work.
- teamSendRuntime: drop `session_stopped` from `FATAL_BLOCK_REASONS` (a stale
stopped slot no longer blocks sending, still shows stopped text); add a
`sessionStopped` option that forces `canSendMessage=true` and `loading=false`.
- TeamChatView: wire the flag through and prefer the stopped prompt so it shows
even when the stale slot has no blocked_reason.
- useTeamWarmup / teamMembershipMutationBusy: handle `'stopped'` explicitly
(keep warmup phase; treat as non-mutation-busy).
- tests: cover the recoverable-stopped runtime, the event-driven flag and its
self-heal, membership non-busy handling, and update the send-box mapping test
for the intentional `session_stopped` behavior change.
Co-authored-by: zynx <>
* fix(installer): run arch check before registry mutation (#3619)
- E1040 arch guard lived in .onVerifyInstDir, which fires after customInit
has already healed/cleared/repaired an existing install's registry; an
arm64 build run on an x64 machine thus mutated the correct-arch install's
registry and uninstaller before aborting
- Move the guard into preInit (after session-begin, before active-marker
writes and customInit) so a wrong-arch installer exits without touching
any registry or uninstaller state
- Report the real detected arch (arm64/x64/x86 via IsWow64Process2) instead
of the hardcoded actual=non-arm64, so triage can tell x64 from x86
- x64/arm64 rejection policy is unchanged; only timing and diagnostics change
Refs Sentry ELECTRON-3BX
* fix(system): let backend own keep-awake blocker (#3620)
Co-authored-by: zk <zk@users.noreply.local>
* feat(guid): expand assistant more dropdown into responsive multi-column panel (#3621)
* feat(guid): expand assistant more dropdown into responsive multi-column panel
* feat(guid): open assistant overflow panel on pill bar hover
* feat(guid): add subtle entry animation to assistant overflow panel
* feat(guid): hide overflow search until assistant list exceeds five rows
* feat(guid): task-oriented default prompts with refined suggestion styling (#3622)
* feat(guid): replace default assistant prompts with task-oriented suggestions
* style(guid): restyle prompt suggestions as plain text with hover arrow
* style(chat): use corner-right-up icon for send buttons
* chore: bump version to 2.1.38 and aioncore to v0.1.49 (#3625)
Co-authored-by: zynx <>
* fix(settings): keep agent repair panel mounted during background revalidation (#3624)
* chore(docs): update WeChat group QR code to wx-17 (#3623)
* chore(docs): update WeChat group QR code to wx-17
* fix(docs): rebuild wx-17 with proportional resize (no distortion)
* fix(docs): tighten wx-17 crop so QR fills frame like the right side
* fix(chat): restore arrow-up icon on send buttons (#3627)
* feat(feedback): route-aware module preselection and ask-the-butler chip on error surfaces (#3626)
* fix(feedback): map every route to its feedback module for the titlebar report button
The titlebar report button only knew 3 routes (conversation/team/settings),
so scheduled tasks, assistants and every settings tab fell through to no
module or a blanket system-settings. Extract the mapping into
resolveFeedbackModule with full route coverage; a unit test walks Router.tsx
so future pages cannot silently miss a module.
* feat(feedback): add ask-the-butler chip to conversation error surfaces
Every conversation error (structured tips, plain/JSON error tips, tool
failures, agent status errors) now shows a Butler chip next to the report
pill. Clicking routes to the home chat with the Butler selected and a
diagnosis prompt containing the error text pre-filled — same flow as the
report modal's solve-via-chat action.
The Butler chip shows on all errors, including user-environment ones that
opt out of reporting via feedback_recommended=false: those are exactly what
the Butler diagnoses best, while the report pill stays hidden for them.
* test(e2e): extend ACP stream injector with error fabrication and cover butler chip
Add emitErrorTip/emitToolError/emitAgentStatusError to the E2E stream
injector so tests can fabricate the three conversation error surfaces
without a real broken agent, and add a spec that walks the butler chip
end to end (error bubble -> home chat with diagnosis prompt seeded).
* test(e2e): repair drifted feedback specs
Four cases were failing on main: the About row label changed
(bugReport i18n copy), the custom-agent editor entry became a
TalkToButlerButton dropdown, and scenarios 5/6 asserted a feedback pill
that was deliberately removed from InlineAgentEditor in #3448. Update
selectors and expectations, and scope modal lookups to the visible
instance since multiple FeedbackReportModal mounts coexist.
* feat(settings): promote Kimi/Moonshot placement in platform and agent lists (#3629)
* feat(settings): move Moonshot platforms to top of model platform list
* feat(settings): pin Kimi after the builtin agent in the Agents list
* feat(settings): default add-model platform to the first list entry
* fix(preview): render distinct heading texts in markdown preview (#3630)
Streamdown's built-in heading components are memoized by node position
only and ignore children, so headings kept stale text when positions
were dropped (rehype-raw) or identical across re-renders. Override
h1-h6 with plain components that keep the built-in classes but always
render the current text.
* feat(github): automated issue/PR/discussion triage to module owners (#3631)
* feat(github): add module dropdown to bug and question issue templates
* feat(github): auto-label and auto-assign issues by module via triage workflow
* feat(github): add CODEOWNERS for module-based PR review routing
* feat(github): welcome-comment and mention duty owner on new discussions
* style(github): apply oxfmt formatting to triage map
* fix(github): use English-only module dropdown with exact-match triage parsing (#3636)
* docs(readme): add AionUi × Kimi contributor campaign section (#3643)
* docs(readme): add AionUi × Kimi contributor campaign section
* docs(readme): simplify campaign CTA and relocate Kimi links to platforms section
* docs(readme): inline campaign CTA and move Kimi links to K3 promo line
* docs(readme): emphasize campaign CTA, de-emphasize Kimi promo links
* docs(readme): banner as sole headline, single CTA, footnote promo
* docs(readme): restore Chinese caption as translation for English banner
* docs(readme): restore English campaign caption for SEO/bot indexing
* docs(readme): full campaign section for all 8 remaining locales
* docs(readme): link campaign CTA to X announcement and claim thread
* fix(agent-settings): hide launch path for npx agents and fix repair-panel status banner (#3641)
Two issues in the agent repair panel (AgentRepairPanel):
1. resolveDiagnosticBanner mapped any status other than missing/offline to the
"Connected" success banner, so an `unchecked` agent showed "Connected" in
the panel while the list correctly showed "Not checked". Handle `online`
explicitly and show a neutral info banner for `unchecked` that points the
user to Test Connection (top-right). While unchecked, the panel now shows
only that banner — launch path, env vars and Save & Test are hidden until a
check has run.
2. The launch-path override was offered for npx-bridged agents. Overriding it
forwards the npx wrapper args to the resolved binary and breaks startup, so
it is now hidden for bridge-launched rows (bridge_binary != binary_name),
matching the backend rejection.
Adds settings.repair.uncheckedTitle/uncheckedHint across all 13 locales and
regenerates i18n key types.
Co-authored-by: zk <zk@users.noreply.local>
* fix(workspace): stable file tree — expand state, search, preview panel (#3642)
* fix(workspace): restore pre-search expanded state on search clear
Searching a broad term opens dozens of directories; clearing the box
previously called refreshWorkspace() with all those dirs still in
expandedKeys, causing a burst of parallel IPC fetches and visible jank.
Now useWorkspaceSearch snapshots expandedKeys just before the first
search keystroke and restores it when the box is cleared, so the
refresh only refetches the small set of dirs that were open originally.
* feat(workspace): incremental tree refresh and per-project expansion cache
applyFreshListings: refresh re-fetches root + all expanded dirs in parallel
and splices fresh content into the existing tree. Expanded dirs stay open and
newly created files appear immediately — no more collapse-on-refresh and no
more stale children blocking new entries.
workspaceTreeCache: module-level LRU (20 entries) keyed by workspace path.
The tree and expandedKeys snapshot survives React remounts triggered by SWR
revalidation, so switching conversations within the same project restores the
exact tree state without a visible flash or reset.
Also includes buildSearchTree (frontend recursive file-name search at any
depth) and collectExpandedDirs (used by the refresh logic to identify dirs
that need a re-fetch).
* refactor(workspace): rewrite tree and events hooks for stable cross-conversation state
useWorkspaceTree: hydrates initial state from workspaceTreeCache on mount;
writes the cache snapshot on unmount and after every successful refresh (not
on every expand/collapse, which caused jank in large workspaces); detects
workspace path changes without remount and re-seeds from the cache.
useWorkspaceEvents: now tracks workspace path instead of conversation_id.
Same-workspace conversation switches only clear selection and call
refreshWorkspace; different-workspace switches also bump treeKey to force
re-render. Removes the backend search provider listener (search moved to
frontend).
* feat(preview): keep file preview open across same-workspace conversation switches
PreviewContext gains closePreviewIfWorkspaceChanged, backed by a stable ref
(lastWorkspaceRef) that survives React remounts. Switching to a conversation
in the same project keeps the preview panel open; switching to a different
workspace or project closes it.
Layout.tsx closes the preview unconditionally when the user navigates away
from /conversation/ routes (to team, settings, etc.), bypassing the workspace
comparison which would no-op when lastWorkspaceRef was never set on those
routes.
* perf(workspace): eliminate per-node O(N) expand lookup and inline renderer churn
expandedKeys.includes() inside renderTitle did a linear scan of the
expandedKeys array for every visible node on every render; with 100+
expanded dirs this meant tens-of-thousands of iterations per
expand/collapse, causing visible jank.
Fix: derive expandedKeysSet (Set<string>) from expandedKeys via useMemo
so FileTypeIcon gets an O(1) .has() check instead.
Also memoize treeData (flattenSingleRoot), treeIcons, and renderTitle
with useMemo/useCallback so Arco Tree does not rebuild all node
renderers on every expandedKeys state update.
* revert: perf(workspace): eliminate per-node O(N) expand lookup and inline renderer churn
Reverts commit 46984d606. The memoization changes broke existing
interaction behaviors; reverting to restore correct functionality.
* fix(preview): correct cross-workspace close behavior for teams and conversations
Bug 1 — preview not closing between teams with different workspaces:
TeamPageContent now calls closePreviewIfWorkspaceChanged(effectiveWorkspace)
whenever its workspace changes, mirroring what conversation/index.tsx does.
lastWorkspaceRef in PreviewContext is updated on every team navigation, so
switching from team A (/foo) to team B (/bar) correctly triggers a close.
Bug 2 — preview incorrectly closing when switching between conversations
of the same workspace:
Sider.handleConversationSelect was unconditionally calling closePreview()
on every conversation click. Removed that call. The workspace-aware
closePreviewIfWorkspaceChanged in conversation/index.tsx is the sole
decision point: same workspace → keep open, different workspace → close.
* refactor(workspace): remove dead mergeLoadedChildren code
applyFreshListings supersedes mergeLoadedChildren; no callers remain.
* feat(workspace): restore multi-tab file preview
Clicking a second file now opens a new preview tab instead of
replacing the first, matching the previous behavior before the
workspace refactor.
* fix(workspace): persist lazy-loaded children and fix minor memory leaks
- loadMore now writes the updated tree to workspaceTreeCache so lazy-
expanded subdirs survive a subsequent refresh or cache rehydration
- flattenSingleRoot is now memoized (useMemo) to avoid a redundant
full-tree traversal on every expand/collapse re-render
- setLoadingHandler stores the debounce timer in a ref and clears it
on unmount, preventing setState-on-unmounted-component warnings
* test: add PreviewContext mock to Layout and TeamPage DOM tests
Layout.tsx and TeamPage.tsx now call usePreviewContext; mock it in
unit tests that render these components without a PreviewProvider.
* fix(conversation): show sign-in hint for empty ACP turns needing auth (#3644)
Adds the ACP_EMPTY_TURN_NEEDS_AUTH agent-tip copy (with a {{hint}} param for the
agent's advertised login instruction) across all 13 locales, and regenerates the
i18n key types. Renders the actionable hint the backend now emits when an agent
ends a turn empty because it isn't signed in, replacing the blank "no visible
reply" tip.
Co-authored-by: zk <zk@users.noreply.local>
* feat(settings): configure model capabilities (#3639)
* feat(settings): configure model capabilities
* fix(settings): preserve automatic vision detection
* fix(settings): use dropdowns for model capability choices
* test(settings): cover model capability configuration
* test(settings): cover capability fallback branches
* fix(github): never auto-assign bonus-labeled issues in triage workflow (#3647)
* feat(github): post claim invitation when an issue is labeled bonus (#3649)
* fix(startup): stop false "local data repair failed" alarm from concurrent startup (#3650)
A second AionUi instance that lost the single-instance lock still registered
the backend startup flow, spawning a competing aioncore over the same data
directory. The losing bootstrap of assistant storage then failed transiently
and was unconditionally misclassified as fatal local data corruption, showing
users a panic-inducing "reinstall may not fix this" dialog (Sentry 135525166).
Layer 1 — single-instance gating:
- Extract shouldRegisterBackendStartup(gotTheLock) into a testable module and
gate the whenReady -> handleAppReady backend startup registration on it, so a
lock-losing instance never spawns a competing aioncore.
Option A contract — bounded peer retry:
- backend-launcher treats the benign BOOTSTRAP_PEER_ALREADY_RUNNING boundary
code as non-fatal and retries startup with bounded backoff; non-peer errors
are thrown immediately with no retry.
Layer 3 — narrow the classifier and soften the copy:
- Replace the unconditional assistant-bootstrap -> backend_local_data_repair_failed
mapping with a narrow classifier that only flags a transient concurrent
startup (peer-yield code, or BOOTSTRAP_SERVER_FAILED at the new
router.assistant.bootstrap.concurrency_contended stage). A plain bootstrap
failure now falls through to the generic bucket, and genuine corruption paths
are unchanged.
- Add a gentle backend_transient_concurrent_startup reason and dialog kind with
i18n copy ("data is safe, no reinstall needed, retry or restart") across all
13 locales.
Tests: single-instance gating unit test, backend-launcher peer-retry tests, and
a rewritten classifier test covering peer/contention cases, the false-alarm
regression, and the preserved corruption paths.
Co-authored-by: zynx <>
* chore: bump version to 2.1.39 and aioncore to v0.1.50 (#3660)
Co-authored-by: zynx <>
* fix(update): allow minimizing active downloads (#3663)
* fix(chat): bound HorizontalFileList to conversation width to prevent overflow (#3659)
A user message with many attached files stretched the whole conversation
area and produced a page-level horizontal scrollbar. The multi-file
HorizontalFileList had no effective min-width / max-width constraint, so
its own width grew with the content and pushed the ancestor layout open
instead of scrolling internally.
Bound the root and scroll container with min-w-0 / max-w-full / w-full
so the flex item can shrink below its content size and the existing
inner horizontal scroll + scroll buttons do their job. Also bound the
wrapping div in MessageText so the ancestor chain allows shrinking.
Closes #3656
Co-authored-by: 黄肠粉 <changfenhuang@huangchangfendeMacBook-Air.local>
* fix(chat): restore ACP file change panels (#3665)
* feat(team): show running state in sidebar (#3666)
* feat(cron): add scheduled-task action to history (#3674)
* feat(desktop): support image avatars for custom agents (#3599) (#3667)
The custom CLI agent editor only exposed an emoji picker, so users could
not pick an image avatar for their custom agents (issue #3599).
Reuse the assistant editor's avatar pattern in InlineAgentEditor:
- emoji picker (existing),
- a built-in image gallery sourced from builtin assistants that ship an
image avatar (/api/assistants/{id}/avatar), and
- an Upload image button.
Because the agent backend has no /api/agents/{id}/avatar route and
resolveAssistantAvatar deliberately rejects raw local file paths, the
uploaded image is read via the existing /api/fs/image-base64 endpoint and
downscaled client-side (canvas, longest edge <=256px, JPEG q=0.85) into a
data: URL stored in the icon column. SVGs are stored verbatim.
All 25 downstream render sites already resolve via resolveAssistantAvatar /
resolveAgentAvatar, which treat data: URLs as images, so no other view
needed changes. The editor preview now renders <img> for image values
instead of dumping the data URL as text.
No backend, schema, or i18n-key changes; the gallery reuses the same
settings.assistantAvatar* keys as the assistant editor.
Co-authored-by: 黄肠粉 <changfenhuang@huangchangfendeMacBook-Air.local>
* fix(i18n): add discoverability hints to input placeholders (#3658)
* fix(i18n): add discoverability hints to input placeholders
Append / command hint to welcome.placeholder and ↑/↓ message-history
hint to sendbox.hint across all 8 supported locales so the slash-command
and history-navigation affordances are discoverable.
Closes #3655
* fix(i18n): complete discoverability hint translations
---------
Co-authored-by: 黄肠粉 <changfenhuang@huangchangfendeMacBook-Air.local>
* feat(team): dormant teammate UI with lazy warmup and per-member retry-start (#3712)
* feat(team): dormant teammate UI and per-member retry-start entry
Frontend support for leader-only warmup:
- add 'dormant' to TeamAgentRuntimeStatus and TeammateStatus; normalizeTeamStatus
passes it through instead of collapsing to idle.
- AgentStatusBadge renders a distinct hollow/outlined dormant dot.
- useTeamSession surfaces the dormant runtime status on the badge; dormant is
non-busy for membership mutations.
- ipcBridge.team.attachAgent posts the per-member attach route; the send box
shows a 'retry start' button on runtime_failed that triggers a directed
per-member attach (not warmupSession), threaded through ThoughtDisplay.
- add i18n key team.work.retryStart across all supported locales.
Tests: dormant mapping + non-busy handling; retry-start invokes the attach route.
* fix(team): clear stale per-slot work via team.slotWorkChanged
The team column's "processing" spinner + timer is driven by
slotWorkBySlot[slot], which useTeamRunView only refreshed from team.run*
events (bundled slot_work) or a full reconcile. Run-less work — e.g. the
leader self-waking to drain its mailbox after a membership change — emitted no
run event on its running->idle transition, so a leader whose completed run's
terminal snapshot showed it still running stayed "processing" until the view
was remounted (which reconciles).
Consume the new backend team.slotWorkChanged event: merge the one slot into
slotWorkBySlot verbatim, so run-less transitions clear the spinner without a
reconcile. Run events keep their whole-map replace semantics.
Tests: useTeamRunView clears an orphaned running slot on slotWorkChanged and
merges a single slot while ignoring other teams; add the event to the mock
surfaces that render the hook.
* fix(team): keep retry-start button visible beside a long runtime-failed status
The inline runtime-failed status text and the retry-start button shared
one flex row where the text had no shrink/truncate and the button had no
shrink guard, so a long or localized status could push the button out of
a narrow parallel-view column. Give the status min-w-0/flex-1/truncate
with a title tooltip for the full text, and pin the button with
flex-shrink-0. Add DOM tests for long-status truncation and the
no-button (plain status) regression case.
* fix(team): swap add-member row plus for inline spinner while pending
Arco Button's loading prop injected a spinner at the start of the row's
flex content, which broke the add-member row layout. Keep the fixed-size
right icon box and swap the plus for an inline Spin while that row's add
is in flight, so the row geometry never shifts. Disable the row while
pending instead of relying on the button's loading state.
* style(team): apply oxfmt formatting to changed team files
---------
Co-authored-by: zynx <>
* feat(permissions): redesign request panel (#3676)
* feat(permissions): redesign request panel
* fix(permissions): compact request interactions
* fix(permissions): simplify request panel
* feat(assistants): support reordering enabled assistants (#3696)
* feat(assistants): support reordering enabled assistants
* refactor(assistants): card grid for My Assistants, single ordering surface
Consolidate assistant ordering to the Enabled tab and drop the competing
drag-reorder on the My Assistants tab:
- My Assistants now renders a card grid (mirrors the Official cards) instead
of a draggable row list; Created-by-you group on top, Your CLI below.
- Remove dnd-kit drag/reorder from My Assistants (MyAssistantRow deleted,
onReorder wiring removed from AssistantHomeTabs and the settings page).
- Update the My Assistants hint copy across all locales (drop 'drag to reorder').
Ordering now lives in exactly one place (the Enabled tab).
* refactor(assistants): simplify Enabled-tab drag affordance
- Keep the drag handle always visible on each row instead of revealing it
only on hover (removes the empty gap at the left of every row).
- Match the drag visuals to the previous My Assistants list: dragged row
just dims (opacity 0.72) and dnd-kit shifts siblings; drop the primary
border highlight and the insertion line that felt heavy.
- Remove now-unused drag-over/active state and props.
* feat(assistants): unify Enabled-tab runtime style + inline enable toggle
- Show the runtime engine as 'Agent: {logo}' (same RuntimeBadge style as the
Official / My Assistants cards) instead of a bespoke backend-name label.
- Add an enable switch to each Enabled-tab row so users can disable an
assistant in place; disabling removes it from the Enabled list.
- Thread onToggleEnabled through AssistantHomeTabs → EnabledAssistantsList.
- Update tests for the new runtime style and in-row toggle.
* style(assistants): apply oxfmt formatting
* test(assistants): cover MyAssistantCard and RuntimeBadge branches
Add DOM tests for the My Assistants card (agent-unavailable warning, delete gating by source, enabled-gated Chat button, click/toggle/delete handlers) and the runtime badge (showLabel/showName branches, acp_backend precedence, logo vs robot fallback), covering presentation branches left uncovered by the reorder patch.
* refactor(assistants): drop unused sort_order reorder path
Ordering now flows through reorderEnabledAssistants + the enabledOrder client preference; the old sort_order-based reorderAssistants had no remaining consumer. Remove it along with the now-dead applyAssistantSortOrders/buildAssistantSortUpdates helpers (and ASSISTANT_SORT_ORDER_GAP) plus their tests. reorderAssistantList is kept since the new path still uses it.
---------
Co-authored-by: 瓦砾 <icey.liu1994@gmail.com>
Co-authored-by: zynx <>
* feat(session-port): AionUi frontend support for the direct-CLI claude/codex session path (#3572)
* fix(acp): fill model picker on async catalog via acp_config_option
The direct-CLI backends (claude/codex) resolve their model/mode catalog
asynchronously after session open and push it as an `acp_config_option`
stream frame. Handle that frame type in both the message transformer and
the ACP message hook: transformMessage ignores it (it is consumed by
useAcpConfigOptions), and useAcpMessage must NOT let it fall through to
the default arm, which would setRunning(true) and light a spurious
timer bar. Mirrors the existing acp_config_changed handling.
* fix(installer): align win32 codex-acp verify path with @agentclientprotocol package
AionCore migrated managed codex-acp from @zed-industries/codex-acp@0.16.0 to
@agentclientprotocol/codex-acp@1.1.2, whose codex binary ships under
@openai/codex-<key>/vendor/<triple>/bin/codex.exe instead of the legacy
@zed-industries/codex-acp-<key>/bin/codex-acp.exe. The Windows bundled-resource
verify still expected the old path, so afterPack failed with 'Missing bundled
resources' on win32 only (mac/linux skip the platform-executable check). Update
the path to mirror AionCore's codex_platform_binary_path.
* test(installer): update codex-acp verify fixtures to @agentclientprotocol layout
Follows the verify-script fix in 8210ee6a8: the managed codex-acp package moved
from @zed-industries/codex-acp@0.16.0 to @agentclientprotocol/codex-acp@1.1.2,
whose Windows binary lives at @openai/codex-win32-x64/vendor/<triple>/bin/codex.exe.
Update the test fixtures (version, entrypoint, platform executable path) to match
so the win32 platform-binary assertion validates the current package layout.
* fix(acp): render backend Notice as an advisory tip without a spurious timer
The AionCore session path now surfaces a rejected mode/model/effort switch (and
codex out-of-turn warnings/deprecations) as a `tips` frame instead of silently
dropping it. useAcpMessage had no explicit `tips` case, so such a frame fell into
the `default` arm which calls setRunning(true) — lighting a timer bar with no
terminal to clear it, since a config-reject Notice arrives while idle (dispatched
by the PUT /config-options path, not a turn). Add a `tips` case that merges the
advisory for display without touching turn state, mirroring the guard the
`acp_config_option` case already applies. Error-severity tips are still handled
earlier by isErrorTipMessage; only info/warning advisories reach here.
Test: useAcpMessage renders an advisory tips notice without lighting running.
* fix(chat): map tool_call status 'canceled' to canceled instead of pending
The backend now closes tool calls left open by an interrupted turn with a
terminal 'canceled' status; normalizeToolCallStatus previously fell through
to 'pending' for it. Mapping it to 'canceled' stops the View-Steps spinner
(hasRunningToolMessages) after a cancel.
* fix(agent): fall back to raw mode value when current mode matches no option
An out-of-catalog current mode value (e.g. backend mode-vocabulary
drift such as codex agent-full-access vs catalog full-access) made
getCurrentModeLabel return an empty string, so the compact pill
rendered a bare "权限 · " with nothing after the separator. Surface
the raw value instead so the anomaly stays visible rather than blank.
* feat(build): verify bundled CLIs (schema v2, claude/codex) instead of acp tools
* chore(build): remove orphan CDN script; port Windows install verifier to CLI contract v2
- delete unused scripts/prepare-managed-acp-tools.sh + its test (zero consumers)
- verify-bundled-aioncore-install.ps1: clis replace acpTools, schemaVersion 2,
duplicate_cli_name / missing_required_cli, executable replaces platformExecutable
* chore(build): drop stale ACP vendor sign-exclusions (superseded by generic vendor glob)
Bundled claude/codex CLIs ship via extraResources and are signed by osx-sign
under mac.hardenedRuntime + entitlements.plist (allow-jit / disable-library-validation
/ allow-dyld-environment-variables already present), same path as the aioncore binary.
Signed-build Gatekeeper validation is the P5 joint verification step.
* style: prettier format verify test
* chore: bump aioncoreVersion to v0.1.51
---------
Co-authored-by: zhangruofan <zhangruofan@bituniverse.org>
Co-authored-by: zynx <>
Co-authored-by: zk <zk@users.noreply.local>
* chore: bump version to 2.1.40 and aioncore to v0.1.51 (#3716)
Co-authored-by: zynx <>
* feat(shortcuts): add common UI bindings (#3675)
* feat(shortcuts): add common UI bindings
* test(shortcuts): cover fallback branches
* fix(shortcuts): handle native settings and temporary workspaces
* fix(shortcuts): remove settings binding
* fix(shortcuts): align sidebar target guard
* fix(team): extend ITeamRunEvent.source with system_lifecycle (#3721)
The backend now emits team runs whose source is "system_lifecycle" (system
and lifecycle wakes), alongside the existing "user_message". Widen the
hand-written ITeamRunEvent.source literal type to the full union so the event
type stays accurate and tsc does not narrow it away where the field is read.
No logic change: no code branches on the run source value, teamMapper does
not map or validate it, and system runs surface through the existing
useTeamRunView / useSiderTeamRunning event chain with no render changes.
Co-authored-by: zynx <>
* feat(notification): notify on agent turn completion when window is unfocused (desktop) (#3715)
* feat(notification): notify on agent turn completion when window is unfocused (desktop)
Desktop users previously got no notification when an agent finished a turn while
the app was in the background — the WebUI `useBrowserNotification` path bails out
on Electron, and the native notification primitive was never wired to the
turn-finished event.
- Reuse the existing turn-finish detection: generalize
`createBrowserNotificationController` to take an injected `shouldShow`
predicate (WebUI derives it from the browser gate) and carry the notification
`kind` in the payload, so no second detection mechanism is introduced.
- Add `useDesktopTurnNotification` (mounted in Layout, Electron-only): on turn
finish it calls `ipcBridge.notification.show`; scoped to turn completion.
- notificationBridge: skip when the main window is focused, keep the existing
`system.notificationEnabled` check, and use a real Electron notification whose
click focuses the window and emits `notification.clicked` (handled by the
existing `useNotificationClick`) to navigate to the conversation.
- Reuse existing i18n keys; no new strings.
Fixes #3709
* fix(notification): guard against missing ipcBridge.conversation in desktop hook
The desktop turn-notification hook is mounted in Layout; tests that render
Layout with a partial ipcBridge mock (no conversation namespace) hit a
TypeError on ipcBridge.conversation.responseStream. Use optional chaining so
the hook safely no-ops when the conversation bridge is absent.
---------
Co-authored-by: GoldenDecade <then_0904@qq.com>
* chore: bump version to 2.1.41 and aioncore to v0.1.52 (#3728)
Co-authored-by: zynx <>
* chore(docs): update WeChat group QR code to wx-18 (#3746)
- Replace wx-17.png with wx-18.png across all README files
- Update references in main README and localized versions
- Remove outdated wx-17.png image
* feat(permissions): submit permission decision in one click for one-off options (#3686)
* feat(permissions): submit permission decision in one click for one-off options
Rebuild the one-click submit on the new PermissionRequestPanel: one-off
options (allow-once / reject-once) submit on a single click, while
permanent and neutral options keep the two-step select-then-confirm flow
to guard against accidental grants. Both the aionrs and ACP paths share
this panel, so the behavior is wired in one place.
The row click is handled in the capture phase because Arco's Radio stops
click propagation, so a bubbling-phase handler would never fire.
* refactor(permissions): submit every option on a single click, drop confirm step
Per maintainer feedback on PR #3686: replace the mixed one-off-vs-permanent
click model with a uniform single-click Button group. Every option (allow,
reject, once, always, neutral) submits immediately on click; there is no
separate confirm button. Matches the interaction model used by other AI
coding tools (Claude Code, Cursor, Copilot) and keeps the panel accessible
by keyboard by construction (each option is a real, focusable button).
The in-flight guard and request/option epoch guards are unchanged.
---------
Co-authored-by: GoldenDecade <then_0904@qq.com>
* feat(tray): left-click tray icon toggles show/hide on Windows/Linux (#3726)
Matches Discord/Slack: tray left-click shows when hidden/minimized and
hides when visible. macOS keeps click → menu convention. Double-click
always focuses. Exports shouldShowFromTray for unit tests.
Co-authored-by: gaursomya777-dev <gaursomya777-dev@users.noreply.github.com>
* fix(conversation): wrap long unbroken url/path in user message bubble (#3727)
A long no-space string (URL/path) rendered with break-words
(overflow-wrap: break-word) gets no break opportunity, so it stays on
one line, stretches the user-message bubble past the window edge, and
the start of the string is clipped off-screen.
Switch to overflow-wrap: anywhere, which breaks a long unbroken run by
character and shrinks min-content so the bubble stays within the row;
normal CJK/English paragraphs are unaffected. The AI-reply (Markdown)
side already uses overflow-wrap: anywhere in ShadowView, so only the
user plain-text path needed the fix.
Fixes #3724
Co-authored-by: GoldenDecade <then_0904@qq.com>
* feat(assistant): show quick-chat button on enabled tab rows (#3748)
The "Enabled" tab rows previously had no way to start a conversation on
hover, unlike the "My Assistants" and "Official" tabs which surface a
"Chat" button. Wire `onStartChat` through to `EnabledAssistantsList` and
render the same hover-revealed chat button on each enabled row, matching
the existing behavior and styling.
* fix(i18n): soften empty-turn needs-auth copy and add token-limit tip (#3751)
For Sentry 136586749 / ELECTRON-3R1: an empty CodeBuddy team-collaboration turn
was rendered as a hard "the agent may need you to sign in — Login with iOA"
assertion, misdirecting users whose backend channel was actually online and
authenticated toward the wrong fix.
- Soften ACP_EMPTY_TURN_NEEDS_AUTH into a dual-purpose fallback across all 13
locales: if you are already signed in it is likely a transient backend or
connection issue (retry or start a new conversation); if not, you can sign in.
The {{hint}} interpolation is preserved so the kilo-style login guidance stays.
- Add ACP_EMPTY_TURN_TOKEN_LIMIT across all 13 locales (possibility wording, no
hint) for AionCore's new token-limit attribution of empty turns.
- Regenerate i18n-keys.d.ts.
Co-authored-by: zynx <>
* fix(startup): skip mkdir for pre-existing backend startup directories (#3759)
A Windows drive root set as work dir (e.g. D:\) made every backend
startup fail: CreateDirectory on a drive root reports access-denied
rather than already-exists, so mkdirSync throws EPERM even with
recursive:true and even though the directory is fully usable. Stat
first and skip mkdir for directories that already exist.
Fixes ELECTRON-3S4
Co-authored-by: zk <zk@users.noreply.local>
* feat(skills): explain delete scope in skill delete confirm dialogs (#3761)
Deleting a skill is a DB-record soft delete: it only stops new
conversations from selecting the skill, while conversations that
already selected it keep working. Surface this in both the single
and batch delete confirm dialogs so users are not afraid deletion
breaks their existing conversations.
Co-authored-by: zk <zk@users.noreply.local>
* chore: bump version to 2.1.42 and aioncore to v0.1.53 (#3765)
Co-authored-by: zynx <>
* fix(webui): implement dialog.showOpen so file and folder pickers work (#3766)
* fix(webui): implement dialog.showOpen so file and folder pickers work
`show-open` is an Electron-only IPC channel. In WebUI the bridge speaks over
a WebSocket whose server side has no provider for it, so `invoke` never
settles — the promise hangs with no rejection and every picker silently does
nothing. None of the 12 call sites guard on the platform, so this affects
workspace selection, file attachment, conversation export, skill import, CSS
theme import and the agent/assistant editors.
The most visible symptom: picking a project via "Work in project" leaves
`guidInput.dir` empty, so the conversation is created with
`workspace: ""` / `custom_workspace: false`, lands in a temporary workspace
and never shows up under the Projects tab.
Add a server-side picker for WebUI. `dialog.showOpen` now dispatches to a
registered web handler when `window.electronAPI` is absent; Electron keeps
using the native dialog. The picker browses the server filesystem through the
existing `/api/fs/dir` endpoint, so no backend change is needed — and a
browser `<input type="file">` would be wrong here anyway, since the agent runs
on the server and the path must resolve there.
All 12 call sites are fixed without touching them.
Fixes #3762
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* test(webui): cover show-open dispatch and picker path helpers
Extract the picker's pure helpers into webFsPickerUtils.ts so they can be
tested without a DOM, then cover the parts that carry real logic:
- platform dispatch: web handler outside Electron, native dialog inside it,
native fallback when nothing is registered, unregistering, and a cancelled
pick resolving to undefined
- entry normalization across the snake_case/camelCase mismatch between
/api/fs/dir and the IDirOrFile type
- parent-directory walking (trailing slashes, clamped at root)
- extension filtering (case-insensitive, wildcard and empty filter groups)
Verified the dispatch tests fail when the fix is reverted or when the
Electron check is dropped, so they guard against a regression rather than
just executing the code.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: xsyk <mafa.aiu@gmail.com>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
* feat(team): add manual warmup entry to AcpModelSelector read-only pill
* feat(team): add warmup click-to-wake tooltip copy for all locales
* feat(team): thread teammate warmup status and trigger to model selector
* style(team): apply oxfmt formatting to warmup selector changes
* fix(conversation): persist ThoughtDisplay elapsed timer across conversation switches (#3774)
The processing indicator's elapsed time was driven by a component-local
timer anchored at mount time, so switching to another conversation and
back remounted the send box and restarted the counter from zero while
the turn was still running.
Add a module-level per-conversation turn clock that records the turn
start timestamp at send time and survives unmount:
- conversationTurnClock.ts: Map keyed by conversation id; begin keeps an
already-recorded origin so re-entry never resets it, end deletes it at
turn termination.
- useAcpMessage: wrap the exported setAiProcessing to record/clear the
origin, clear it on finish/error/error-tip/disconnect/stop, restore it
when hydration reports the conversation still processing, and drop a
stale origin when hydration reports it idle.
- useAionrsMessage: sync the origin from the combined running state; a
hydrated-conversation guard prevents transient pre-hydration falses
from deleting the persisted origin during a switch, and the hydration
idle path clears stale origins the sync effect cannot see.
- AcpSendBox/AionrsSendBox: feed the origin into ThoughtDisplay's
absolute-timestamp mode (startedAtMs/externalElapsedSource) instead of
the mount-anchored local timer; team mode behavior is unchanged.
As a side effect, the aionrs indicator now shows elapsed time for the
whole turn instead of resetting on each thought-stage change, matching
ACP and team behavior.
Closes #3773
Co-authored-by: zk <zk@users.noreply.local>
* feat(explorer): project-scoped Explorer replacing workspace tree (#3763)
* feat(explorer): project-scoped Explorer replacing workspace tree
Replace the per-conversation Workspace file tree (HTTP getWorkspace data
source, remounted every conversation switch) with a project-level Explorer
hosted at the Layout level, so the file panel persists across same-project
conversation and agent-tab switches without remounting or reloading.
- WS fs/* data plane (subscribe/snapshot/delta + read/write/mkdir/remove/
rename) behind a module store keyed by {pe_id, relative_path}; HTTP
/api/projects control plane for roots + attach/remove
- Layout-level ProjectPanelHost gated on project_id, two-level width clamp
so chat and preview keep their reserve; collapse/mobile/mac-win handling
- Preview panel hoisted into the host row (structurally persistent);
component-switcher tab bar (Files / Changes placeholder) with a pinned
open-externally control
- Rename + delete via WS commands with the WS delta as the single source
(no double consume); project_id backfill picked up responsively through
conversation.listChanged on both conversation and team routes
- Remove the legacy Workspace tree and its per-switch getWorkspace calls;
the getWorkspace endpoint is retained for conversation export
* feat(explorer): add-to-chat + A-paste with source-tagged ChatFileRef
- Send files as ChatFileRef[] (project {pe_id,relative_path} | upload
{path}) across conversation + team + command-queue DTOs, tagged by
source; drop front-end absolute-path/[[AION_FILES]] marker building
so the backend resolves refs at the send edge
- Explorer tree right-click "add to chat" targets the active
conversation's send box (files and directories)
- A-paste: drag/paste OS files import into a pe-targeted dir via
/api/fs/copy {pe_id,relative_path}; all-fail toasts error, partial
warns, conflicts/rejected dirs surface in failed_files
- Fix preview panel opening when clicking a context-menu item: stop
the droplist click bubbling into the tree node's select handler
- team-a: file-selection bus (set + append) carries a target
conversation id; each send box accepts only its own (or broadcast),
the Explorer emits on all agent prefixes with the active column's id,
and TeamPage publishes the active member column — no leak across
same-type team columns; team add-to-chat un-hidden
- Tests: send-box no-leak guard (mutation-verified), add-to-chat
preview regression, A-paste all-fail, ChatFileRef collect/split,
command-queue ref dedup, currentConversation store
* fix(chat): route "Add files" picks as local file refs
The chat send contract sends attachments as source-tagged ChatFileRefs
that the backend resolves at the edge. "Add files" (paperclip picker)
browses the backend machine's own filesystem (native dialog in Electron
/ server-fs browse in WebUI), but its raw path was sent as an `upload`
ref — which the backend rejects as outside its managed upload dir.
Introduce a third `local` kind for backend-machine paths (sent as-is),
alongside `upload` (device uploads/paste/drag → managed dir) and the
existing `project` (Explorer tree) kind:
- chatFile.ts: add `local` kind, localFileRef, chatFileRefPath, and
key/guard branches (upload vs local stay distinct at a shared path)
- fileSelection.ts: localSelectionItems tags picked paths
- Acp/AionrsSendBox: appendSelectedFiles merges picks into atPath as
local refs (this box only, no cross-column emit), not the upload lane
- messageFiles.splitChatFileRefs: rebuild local refs for queue edits
- FileService/PasteService: always upload blob sources to the managed
dir so device/paste/drag paths resolve as valid `upload` refs
- Guid: files model string[]->ChatFileRef[]; handleFilesPicked (local)
split from handleFilesUploaded (upload); default_files still passes
plain paths; initial-message chain carries ChatFileRef[] through with
legacy string[] coercion
Tripwire tests cover local vs upload tagging across the helpers, the
Guid input hook, and the Guid prefill path.
* feat(explorer): sort tree children directories-first (#3775)
Explorer children were shown in the backend snapshot's raw order
(alphabetical with directories and files interleaved), and delta-added
nodes appended at the end. Sort at the buildChildren projection point so
both snapshots and deltas land in order with no cache-order to maintain.
- directories first, then files and symlinks (symlinks grouped with
files, matching the isLeaf = !isDir projection)
- each group by name case-insensitively (localeCompare, sensitivity base)
- pe roots are not reordered (kept in their backend order_index)
Tripwire tests assert dirs-first + case-insensitive per-group order on a
scrambled mixed input, and that roots stay in backend order.
* refactor(webui): reduce redundant API refetch and drop dead front-end fs accessors (#3768)
* fix(swr): disable revalidateOnFocus globally, opt in Google auth
- Add a global SWRConfig (revalidateOnFocus: false) in AppProviders so
the app no longer refetches every query on each window focus. Before,
focus re-hit /api/assistants (twice — two inconsistent SWR keys),
/api/extensions/acp-adapters, /api/settings/client, /api/agents/*,
etc. even though those stay fresh via WebSocket events
(conversation.listChanged, team events, extensions.state-changed) or
in-app mutate after edits. Measured live: 6 focus refetches → 0.
- Opt Google auth + subscription status back into revalidateOnFocus:
they change in an external browser (OAuth / billing) with no in-app
mutate or WS event, so returning to the window is the refresh signal.
- Add a tripwire test asserting the two Google queries carry the opt-in
while the in-app config query does not.
* perf(swr): unify duplicate assistants-list cache key
usePresetAssistantInfo keyed the assistants list on 'assistants' while
useConversationAssistants / useCustomAgentsLoader use 'assistants.list'.
Both fetchers call ipcBridge.assistants.list.invoke() with no params and
the same shape, so the two keys were two SWR caches — the list was
fetched twice, and a mutate to one key left the other stale.
- repoint usePresetAssistantInfo to 'assistants.list' (single cache/fetch)
- drop the now-redundant mutate('assistants') calls that were always
paired with mutate('assistants.list') (useManagedAgents,
SkillRuleGenerator, useAssistantEditor, SkillDetailPage)
- update tests to the unified key
Not touched: the 'assistants' settings-tab id/path (not an SWR key).
* refactor(bridge): drop dead front-end fs/watch bridge accessors
After the Explorer teardown replaced the per-conversation Workspace tree,
several front-end bridge accessors have no renderer/main/common caller.
Remove the unused FE bindings and their orphaned satellites. No backend
endpoints are touched — only the front-end bridge surface.
- ipcBridge.ts: remove fs.readFileBuffer, fs.createTempFile,
fs.removeEntry, fs.renameEntry, the responseSearchWorkSpace stub, the
fileWatch.* group, and the fileSnapshot.* group (+ its mapper import)
- delete utils/file/workspaceFs.ts (no importer)
- delete fileSnapshotMapper.ts + its test (no producer importer once the
fileSnapshot group is gone)
Kept: conversation.activeCount — still used by the main-process tray.
* test(team): mock ChatSlider in TeamPage warmup test to fix flaky CI
The real ChatSlider subtree subscribes to ipcBridge.conversation.* channels
(listChanged, responseStream, turnCompleted) in passive effects that this test
does not stub. ChatSlider mounts asynchronously after the dispatchConversation
SWR resolves, so whether the unstubbed code path ran within the test window was
timing-dependent: it passed locally but crashed in CI's full-suite run with
'Cannot read properties of undefined (reading on)'.
Mock ChatSlider like the other heavy children so the test stays focused on
warmup wiring and no longer races against the incomplete IPC mock.
* test(team): stub conversation.listChanged and updated preview API in warmup test
After merging main, TeamPage subscribes to ipcBridge.conversation.listChanged on
mount and uses usePreviewContext().closePreviewIfScopeChanged (renamed from
closePreviewIfWorkspaceChanged). The warmup test's mocks lagged behind both,
causing deterministic crashes in the PR merge check. Stub the missing IPC
channel and the renamed preview method.
* fix(pet): source enable switch initial state from authoritative value (#3777)
The desktop pet settings switch read its initial "enabled" value from the
possibly-empty configService backend cache with a `?? true` default, while
the startup creation gate required `pet.enabled === true` and the backend
getPetEnabled read defaulted to `?? false`. For users who never toggled the
switch, pet.enabled is unset: the window is never created, yet the switch
displayed ON — a "UI lies" contradiction (Sentry ELECTRON-3SH).
Source the switch's initial value from the authoritative persisted value via
systemSettings.getPetEnabled (ProcessConfig), which returns `?? false`, so the
displayed state matches actual behavior: ON iff a pet window exists. The read
is async, so gate the switch on a resolution flag (loading + disabled with
checked=false until resolved) to avoid flickering to a state opposite the
authoritative value, and fall back to OFF (never ON) on IPC failure.
Only the pet.enabled initial-value source changes; size/dnd/confirm reads and
all toggle handlers are unchanged. The startup gate and backend read are
already consistent with "unset -> OFF" and are left untouched.
Adds a focused jsdom test covering the authoritative-source read, no-flicker
resolution, undefined/true/false mapping, toggle persistence, and fallback.
Co-authored-by: zynx <>
* feat(conversation): restore agent-reported context usage indicator for ACP conversations (#3772)
* feat(conversation): restore ACP context-usage indicator in send box
The ContextUsageIndicator ring was orphaned when the ACP implementation
moved to the backend (#2804 removed the event producer, #2985 removed the
render site). The backend now emits acp_context_usage stream frames
(UsageUpdate passthrough) and serves the latest snapshot via
GET /api/conversations/{id}/usage, so the consumer side can be wired back:
- add ipcBridge.conversation.getUsage for the usage snapshot endpoint
- hydrate tokenUsage/context_limit in useAcpMessage after runtime ensure,
without clobbering values already set by live stream frames
- render ContextUsageIndicator in the SendBox sendButtonPrefix slot,
matching the pre-#2985 wiring
* fix(conversation): require agent-reported window size for the usage ring
Never compute the ring percentage against the hardcoded per-model
default: context_limit is now a required prop, the indicator renders
nothing unless the agent reported a real window size (UsageUpdate.size
> 0), and AcpSendBox gates the slot on both counters being present.
* feat(conversation): show hollow usage ring when agent reports only token count
Agents that report used tokens without a context window size (e.g. gemini
via quota meta) previously got no indicator at all. Now they get a hollow
track ring whose popover shows the raw token count and states the window
size is unknown; the progress arc and percentage remain exclusive to
agent-reported window sizes. Adds tokensUsed/windowUnknown i18n keys to
all locales and focused tests for both indicator modes.
* feat(conversation): show session cost and token breakdown in usage popover
The usage snapshot and acp_context_usage frames now carry the agent's
cumulative session cost and per-turn token counters (input/output/cache/
thinking) under the UsageUpdate _meta field. The indicator popover renders
a cost line and a compact breakdown line in both ring modes; zero-valued
optional counters are omitted. Adds sessionCost/input/output/thought/
cachedRead/cachedWrite i18n keys to all locales and unit tests for the
payload mapper and popover rendering.
* refactor(conversation): drop unused hardcoded model context-limit table
The usage indicator only trusts agent-reported window sizes; the
per-model fallback table lost its last consumer and must not regain one.
* style(conversation): reformat AcpSendBox destructuring after merge
---------
Co-authored-by: zk <zk@users.noreply.local>
* chore: bump version to 2.1.43 and aioncore to v0.1.54 (#3782)
Co-authored-by: zynx <>
* feat(search): filename search + chat-ref (#3784)
- fs/search client: searchStore (supersede/cancel/limit, owner-token
mutual exclusion) + MonitorClient requestWithId/abandon; fs/searchMatch
dispatch isolated from the explorer store
- Front-end ranked flat results; SearchHit maps to a project chat-ref
so a hit adds to chat with zero conversion
- Two skins on one stream: @-mention dropdown (gated on resolvable pe
roots, legacy workspace-list fallback during the load window) and the
Explorer search panel (click = reveal in tree, explicit add-to-chat)
- PE · REL secondary label to disambiguate multi-folder hits
- i18n keys across all locales; focused unit + dom tests
* fix(tray): honor close-to-tray on custom title-bar close (#3669) (#3717)
Custom title-bar close goes through windowControlsBridge IPC and always
called window.close(). On Linux (and other frameless builds) that path
did not reliably minimize to tray when closeToTray was enabled.
Hide the window when close-to-tray is on and the app is not quitting;
fall back to the first live window if focus is lost. Adds unit coverage
for hide/close/quit/focus-fallback paths.
Co-authored-by: gaursomya777-dev <gaursomya777-dev@users.noreply.github.com>
* fix(preview): restore file rendering for Explorer opens (#3786)
- The per-project Explorer's handleOpenFile opened files by {pe_id,
relative_path} over WS fs/read and passed neither file_path nor
workspace to openPreview, so image/pdf/office viewers (which need a
renderable src or a local path) broke; only text survived (ELECTRON-3SZ).
- image: fs/read base64 is bare (no data: prefix) but ImageViewer feeds
content straight into <img src>, so wrap it into a data URL by extension.
- pdf/office: resolve pe -> absolute path via the new fs/resolve command
and pass file_path + workspace so PDF (file://) and officecli watch work.
- Add focused tests for the payload builder (image/pdf/office/text branches).
This is a temporary patch pending the Preview redesign that will consume
{pe_id, relative_path} end-to-end; all added symbols carry a PATCH(ELECTRON-3SZ)
marker so the block greps out cleanly. Depends on aioncore branch
boii/fix/preview-explorer-filepath adding the fs/resolve command.
* feat(skills): add file browser to detail page (#3683)
* feat(skills): add file browser to detail page
* style(skills): make file browser left/right split visible and drop read-only label
- Give the file tree panel a subtle bg so the left/right split reads clearly
(the previous 1px divider used a background-color token that renders transparent)
- Remove the always-empty divider element
- Drop the 'Read only' label from the content header per design feedback
- Use visible border-border-3 for the container/header borders
- Update SkillFileBrowser dom tests accordingly
---------
Co-authored-by: luisinluisao <luisinluisao@users.noreply.github.com>
Co-authored-by: 瓦砾 <icey.liu1994@gmail.com>
* chore: bump version to 2.1.44 and aioncore to v0.1.55 (#3792)
Co-authored-by: zynx <>
* feat(explorer): reveal highlight + @ Tab complete (#3794)
- Reveal: opt the Explorer tree into the workspace-tree full-row selected
highlight (--color-fill-3), and scroll the selected node into view once
it enters the DOM (deduped per selection; tree is not virtualized)
- @ mention: Enter and Tab both accept the active item; the key→action
contract is extracted to the pure resolveAtFileMenuKey for unit testing
- Tests: reveal highlight / scroll-into-view / no-repeat-scroll dom tests,
resolveAtFileMenuKey unit tests
* fix(sendbox): tag loading-window @mention fallback with local chat-ref (#3801)
- The legacy workspace flat-list @mention fallback (used while a project's
pe roots are unresolved) built items with no chatRef, so collectChatFileRefs
fell back to an upload ref carrying the workspace absolute path; the backend
rejects that as outside its managed upload dir → 400 (ELECTRON-3TG)
- Extract workspaceMentionItemFrom…
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.
2.1.36 (2026-07-17)
Desktop
Features
Styling
Core (v0.1.48)
Features
Bug Fixes
Code Refactoring