Skip to content

fix(packaging): verify bundled resources from manifest - #3587

Merged
piorpua merged 4 commits into
mainfrom
aionissue/fix-2.1.33-128134003-001
Jul 14, 2026
Merged

fix(packaging): verify bundled resources from manifest#3587
piorpua merged 4 commits into
mainfrom
aionissue/fix-2.1.33-128134003-001

Conversation

@piorpua

@piorpua piorpua commented Jul 14, 2026

Copy link
Copy Markdown
Contributor

Pull Request

Description

This PR makes AionUi consume AionCore's bundled managed resources manifest as the packaging and install-time source of truth. The verifier now rejects missing manifests, invalid schemas, runtime key mismatches, duplicate tool slugs, missing required tools, unsafe relative paths, mismatched local tool manifest paths, and missing required files or directories.

The change also updates the Windows installer verification script and the legacy managed ACP artifact script so old-version-only Codex ACP bundles cannot pass verification when the current runtime expects @agentclientprotocol/codex-acp@1.1.2.

Related Issues

Type of Change

  • fix - Bug fix (non-breaking change which fixes an issue)
  • feat - New feature (non-breaking change which adds functionality)
  • perf - Performance improvement
  • refactor - Code restructuring (no behavior change)
  • Breaking change (fix or feature that would break existing functionality)
  • docs - Documentation update

Atomic PR Checklist (Rule 1)

  • This PR contains exactly one feature or bug fix that cannot be further decomposed
  • The PR title follows Conventional Commit format: <type>(<scope>): <subject> (English)

Local Checks (Rule 3)

  • bun run format / bun run format:check - formatting passes
  • bun run lint - no lint errors
  • bunx tsc --noEmit - no type errors
  • bunx vitest run - tests pass
  • i18n validated (bun run i18n:types + node scripts/check-i18n.js) - run by just push
  • New/changed user-facing text uses i18n keys (N/A: this change updates packaging, installer verification, scripts, and tests)

Runtime Verification

  • Verified on macOS
  • Verified on Windows
  • Verified on Linux
  • I have performed a self-review of my own code

Screenshots

N/A.

Additional Context

Validation run for this PR:

  • just push -u origin aionissue/fix-2.1.33-128134003-001
  • bun run lint -- --quiet
  • bun run format:check
  • bunx tsc --noEmit
  • node scripts/generate-i18n-types.js
  • node scripts/check-i18n.js
  • bun run test (290 test files passed, 1 skipped; 2187 tests passed, 5 skipped)

Focused verification previously run for the changed packaging and installer paths:

  • bun run lint:fix
  • bun run format
  • bunx tsc --noEmit
  • bun run test tests/unit/assets/verifyBundledAioncoreResources.test.ts tests/unit/assets/verifyBundledAioncoreInstallScript.test.ts tests/unit/assets/prepareAioncoreActionsArtifact.test.ts tests/unit/assets/prepareAioncoreLocalBundle.test.ts tests/unit/assets/prepareManagedAcpToolsScript.test.ts

Release validation caveats:

  • package.json#aioncoreVersion still points at v0.1.46; packaging must use an AionCore artifact that includes fix(managed-resources): emit bundled resource manifest AionCore#617 before release.
  • Real six-runtime release artifact smoke coverage still needs generated or unpacked runtime bundles.
  • Actual Windows installer PowerShell execution still needs to be run on Windows; this PR includes static and unit-test coverage on this host.

zynx added 2 commits July 14, 2026 14:03
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.
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.
@codecov

codecov Bot commented Jul 14, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@piorpua
piorpua merged commit 0c3dc85 into main Jul 14, 2026
15 checks passed
@piorpua
piorpua deleted the aionissue/fix-2.1.33-128134003-001 branch July 14, 2026 09:33
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…
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant