Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .agents/skills/verify-deepseekbot/features/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ One file per user-facing feature. Each file has four sections: `Sub-features`, `
| Bot mode entry and the default DeepSeek Bot | no | `scripts/e2e-bot-mode-sidebar.mjs`, `scripts/e2e-panel-workspace.mjs`, `scripts/e2e-bot-mode-exit.mjs` | [bot-mode.md](bot-mode.md) |
| Bot mode dock (desktop layout, unified Sessions, Channel header, Profile, settings, files and Inbox) | no | `scripts/e2e-panel-workspace.mjs`, `scripts/e2e-channel-sessions.mjs`, `scripts/e2e-bot-subpanels.mjs`, `scripts/e2e-browser-computer-subpanels.mjs`, `scripts/e2e-panel-size-persistence.mjs`, `scripts/e2e-panel-resize-stability.mjs`, `scripts/e2e-nested-channel-panels.mjs`, `scripts/e2e-channel-sessions-desktop.mjs`, `scripts/e2e-bot-mode-sidebar.mjs` | [bot-mode-dock.md](bot-mode-dock.md) |
| Orchestrator model switching | yes, for reply evidence | none | [orchestrator-model-switch.md](orchestrator-model-switch.md) |
| Live IM Bot pickers | no | `verify-app` settings path | [im-bot-pickers.md](im-bot-pickers.md) |
| Private input with mixed local/IM sources | yes, for native tool/approval proof | `scripts/e2e-private-input-origin.mjs` | [private-input-origin.md](private-input-origin.md) |
| Bot DM reply | yes | `scripts/e2e-llm-scenarios.mjs --scenario dm-reply` | [bot-dm-reply.md](bot-dm-reply.md) |
| Bot-to-Bot Group mentions and wake | yes | `scripts/e2e-llm-scenarios.mjs` (nightly) | [bot-group-mention.md](bot-group-mention.md) |
Expand Down
26 changes: 26 additions & 0 deletions .agents/skills/verify-deepseekbot/features/im-bot-pickers.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# Live IM Bot pickers

## Sub-features

- IM app creation and binding offer only live, unpaused Bots.
- A deletion or pause while the picker is mounted removes the option and invalidates an already selected Bot.
- Existing bindings retain paused Bot names; deleted tombstones are not selectable.

## How to get to it (user POV)

Bot settings → IM apps → Create app, or Bind on an unbound connected app → Bot.

## Driving it with verify-app

1. Build the Client, launch a task-owned isolated Host and run `doctor`. Keep ARIA and console evidence for each attempt.
2. Require a qualified IM setup provider or an unbound connected app. Create two disposable Bots through the public owner. Delete one with retained Memory using `bot-delete-preview` / `bot-delete-confirm`; read `botharness/list` to confirm its historical `deleted` marker.
3. Open Create app and its Bot picker; require deleted/paused Bots to be absent and the live Bot present. Repeat Bind.
4. Select a disposable Bot, then delete or pause it while the picker remains open. Require its option to disappear and submission to be disabled. Retain browser evidence and the canonical deletion or pause result.
5. Confirm an existing app bound to a paused Bot retains that Bot's name, rather than its slug.

## Gotchas

- A stock isolated profile may return `apps: []` and `setups: []` from `botharness/messagingApps`. Create app is then intentionally unavailable: record the missing-provider runtime gap.
- Component regression tests reproduce the filtering and mounted-selection bug. The isolated Host confirms retained tombstones, but full Create/Bind UI evidence remains pending without a qualified provider/app.
- Historical roster rows are intentional. Filter picker eligibility before name equality comparison; do not remove tombstones from the canonical roster or change the shared comparator.
- These checks do not prove real IM delivery or native Desktop behavior. No model call is needed for picker assertions.
11 changes: 11 additions & 0 deletions changes/3220-live-im-bot-picker.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
---
section: Fixed
---

## en

- Fixed deleted or paused Bots appearing in the IM app creation and binding pickers; deleting a selected Bot also disables submission until a live Bot is chosen ([#3220](https://github.com/BotHarness/BotHarness/issues/3220)).

## zh

- 修复 IM 应用创建和绑定下拉中仍出现已删除或暂停 Bot 的问题;选中的 Bot 被删除后,须重新选择存活 Bot 才能提交([#3220](https://github.com/BotHarness/BotHarness/issues/3220))。
18 changes: 14 additions & 4 deletions packages/client/src/client/im-apps-section.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -118,11 +118,21 @@ export function ImAppsSection({
t,
}: PropsLocale<'botharness'> &
InjectFace<{ imApps: ImAppsFace; store: ClientStore }>): ReactElement {
const bots = useStoreSlice(store, (state) => state.bots, sameBotNames);
const bots = useStoreSlice(
store,
(state) => state.bots.filter((bot) => !bot.deleted),
sameBotNames,
);
const bindableBots = useStoreSlice(
store,
(state) => state.bots.filter((bot) => !bot.deleted && !bot.paused),
sameBotNames,
);
const [notice] = useState(() => imApps.takeNotice());
const [loaded, setLoaded] = useState<Loaded>({ status: 'loading' });
const [dialog, setDialog] = useState<Dialog>();
const [botSlug, setBotSlug] = useState('');
const hasSelectedBot = bindableBots.some((bot) => bot.slug === botSlug);
const [busy, setBusy] = useState(false);
const [failed, setFailed] = useState(false);
const reload = async (): Promise<void> => {
Expand Down Expand Up @@ -180,7 +190,7 @@ export function ImAppsSection({
disabled={busy}
value={botSlug}
onSelect={setBotSlug}
options={bots.map((bot) => ({ value: bot.slug, label: bot.displayName }))}
options={bindableBots.map((bot) => ({ value: bot.slug, label: bot.displayName }))}
/>
)}
</Field>
Expand Down Expand Up @@ -288,7 +298,7 @@ export function ImAppsSection({
title={t('imApps.bindFor', { app: dialog.app.name })}
labels={labels}
submitLabel={t('imApps.bind')}
submitDisabled={botSlug === ''}
submitDisabled={!hasSelectedBot}
busy={busy}
error={error}
onCancel={dismiss}
Expand Down Expand Up @@ -337,7 +347,7 @@ export function ImAppsSection({
description={t('imApps.createHint')}
labels={labels}
submitLabel={t('imApps.next')}
submitDisabled={botSlug === ''}
submitDisabled={!hasSelectedBot}
error={error}
onCancel={dismiss}
onSubmit={() => {
Expand Down
77 changes: 75 additions & 2 deletions packages/client/test/im-apps-actions.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import { createImApps, ImAppsSection, type ImAppsFace } from '../src/client/im-a
import { zhTranslate } from '../src/client/locale.js';
import { BotSettings } from '../src/client/bot-settings.js';
import { createStore } from '../src/client/store.js';
import { ProviderAppSetup } from '../src/client/provider-app-setup.js';
import { chooseOption, comboboxOption, dialog, openCombobox } from './primitive-mocks.js';

let host: HTMLDivElement;
Expand Down Expand Up @@ -59,7 +60,7 @@ function store() {
return value;
}

async function render(face: Partial<ImAppsFace>): Promise<void> {
async function render(face: Partial<ImAppsFace>, clientStore = store()): Promise<void> {
const imApps: ImAppsFace = {
load: async () => ({ apps: [], setups: [] }),
manage: () => undefined,
Expand All @@ -70,7 +71,9 @@ async function render(face: Partial<ImAppsFace>): Promise<void> {
...face,
};
await act(async () => {
root.render(createElement(ImAppsSection, { imApps, store: store(), t: zhTranslate } as never));
root.render(
createElement(ImAppsSection, { imApps, store: clientStore, t: zhTranslate } as never),
);
});
}

Expand All @@ -83,6 +86,76 @@ function button(label: string, scope: ParentNode = document): HTMLButtonElement
return found;
}

it.each([
{ deleted: false, paused: false, offered: true },
{ deleted: false, paused: true, offered: false },
{ deleted: true, paused: true, offered: false },
])(
'offers only bindable Bots when creating an IM app; deleted=$deleted paused=$paused',
async ({ deleted, paused, offered }) => {
const clientStore = store();
clientStore.setRoster(
clientStore
.getSnapshot()
.bots.map((bot) => (bot.slug === 'bo' ? { ...bot, deleted, paused } : bot)),
[],
);
await render(
{
load: async () => ({
apps: [],
setups: [
{
version: 1,
providerId: 'dsh-im/feishu',
platform: 'feishu',
kind: 'credentials',
endpoint: 'dsh-im/app-setup',
},
],
}),
appSetup: new ProviderAppSetup({
call: async () => {
throw new Error('Unexpected app setup RPC before choosing a Bot');
},
}),
},
clientStore,
);
await act(async () => button('创建应用').click());
await openCombobox('Bot', dialog());

expect(comboboxOption('bo') !== undefined).toBe(offered);
expect(comboboxOption('ada')?.textContent).toContain('Ada');
},
);

it('removes a Bot deleted while the IM bind picker is open', async () => {
const clientStore = store();
await render(
{ load: async () => ({ apps: [app({ ref: 'a', name: 'Lark Sales' })], setups: [] }) },
clientStore,
);
await act(async () => button('绑定 Lark Sales').click());
await chooseOption('Bot', 'bo', dialog());
expect(button('绑定', dialog()).disabled).toBe(false);
await openCombobox('Bot', dialog());
expect(comboboxOption('bo')).toBeDefined();

await act(async () => {
clientStore.setRoster(
clientStore
.getSnapshot()
.bots.map((bot) => (bot.slug === 'bo' ? { ...bot, deleted: true, paused: true } : bot)),
[],
);
});

expect(comboboxOption('bo')).toBeUndefined();
expect(comboboxOption('ada')).toBeDefined();
expect(button('绑定', dialog()).disabled).toBe(true);
});

it('binds an unbound App to a chosen PersonaBot and reloads the list', async () => {
let bound = false;
const bind = vi.fn(async () => {
Expand Down
8 changes: 8 additions & 0 deletions scripts/verification-inventory.json
Original file line number Diff line number Diff line change
Expand Up @@ -682,6 +682,14 @@
"task": 1958,
"gap": null
},
{
"id": "im.apps.bot-eligibility",
"label": "Create or bind an IM app using only live unpaused Bots and invalidate deleted selections",
"source": "packages/client/src/client/im-apps-section.tsx",
"task": 3220,
"guide": ".agents/skills/verify-deepseekbot/features/im-bot-pickers.md",
"gap": "Component regression and canonical tombstones verified; full Create/Bind UI needs a qualified IM setup provider or unbound app, plus native/Human QA."
},
{
"id": "orchestrator-model-switch",
"label": "Orchestrator-only quick model switching with Assignment inheritance",
Expand Down
Loading