Skip to content
Merged
3 changes: 2 additions & 1 deletion .agents/skills/verify-deepseekbot/features/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ One file per user-facing feature. Each file has four sections: `Sub-features`, `
| Disposable Memory history, restore and stale-head refusal | no | `scripts/e2e-memory-restore.mjs` | [memory-restore.md](memory-restore.md) |
| Version-bound Memory images, Markdown previews and CLI export | no | `scripts/e2e-memory-images.mjs`, `scripts/e2e-memory-markdown.mjs`, `scripts/e2e-memory-formats.mjs` | [memory-images.md](memory-images.md) |

| Activity Center: approvals, Channel mute and independent control-rail indicator; page and Dock | yes (real pending requests) | `scripts/e2e-inbox-tool-approval.mjs`, `scripts/e2e-activity-live.mjs`, `scripts/e2e-activity-approve-sync.mjs`, `scripts/e2e-activity-load.mjs` | [activity-inline-approval.md](activity-inline-approval.md) |
| Activity Center: approvals, Remote Host actions, Channel mute and independent control-rail indicator; page and Dock | yes (real pending requests) | `scripts/e2e-inbox-tool-approval.mjs`, `scripts/e2e-activity-live.mjs`, `scripts/e2e-activity-approve-sync.mjs`, `scripts/e2e-activity-load.mjs` | [activity-inline-approval.md](activity-inline-approval.md) |

## Coverage and maintenance

Expand Down Expand Up @@ -94,3 +94,4 @@ Historical failures from 2026-10-14 (not requalified by this audit):

| Three-action tool approval and coherent Full Access | yes | `scripts/e2e-tool-approval-simple.mjs` | [tool-approval-simple.md](tool-approval-simple.md) |
| Workspace request read/write approval and original-task continuation | yes | `scripts/e2e-workspace-request-write.mjs`, `scripts/e2e-mobile-folder-browse-web.mjs` | [workspace-request-write.md](workspace-request-write.md) |

Original file line number Diff line number Diff line change
Expand Up @@ -131,3 +131,13 @@ Seed Group messages and Human mentions through a real Bot turn using `channel_se
Right-click the desktop control-rail Activity button, or focus it and press Shift+F10/ContextMenu. Verify All notifications, Needs you, Related to you, Other unread and Off as a single selection. Context-menu open must not toggle the Activity panel; Escape/selection must restore focus and outside dismissal must keep the new target's focus. Off hides only the dot: normal click and the shortcut still open Activity, and the accessible label retains true counts. Read retained replies and handled requests never light the corresponding scope.

Use real ordinary messages, Human mentions/replies and a pending Group join request. Compare every indicator scope against CLI `inbox-status` messageCounts and existing Inbox queries. Mute/unmute through the Channel menu and CLI, mark the latest message read, and resolve the disposable join request; verify status refresh changes the dot while filters remain independent. Change Activity search/category and clear its filters, then change the indicator scope; neither setting may overwrite the other. Check persistence after reload, same-origin second-window synchronization, and a different origin or browser context staying independent. Invalid stored values return to All. A deliberately failed status request must show unknown rather than a fabricated zero; Off remains off through failure/recovery. Label this injected transport failure separately from real outages. Capture menu and dot states in light/dark and inspect browser errors and canonical read state. The narrow native sidebar entry is outside the control-rail preference's scope.

## Remote Host Activity actions (#2500)

The page and Dock group each paired Remote Host's Needs you, For you and Other unread under its Host name and a Remote mark. HumanInboxView receives a Host-scoped source for identities, commands and request cards. Approvals and questions reuse the existing cards with the shared request module; join requests, snooze and clear use shared attention-actions. Source links open the remote conversation, not an exact historical message. Search, category, history and sorting query each Host. The Bot and Channel pickers contain This Host's identities, so either filter hides remote groups.

Use two task-local Hosts with different home basenames and ports. Pair B from A through Add remote in the header add menu. Retain the browser profile for baseline and changed captures. Give their built-in Tob different display names and use distinct DM messages to prove same-slug isolation. Use disposable ordinary Bots with deepseek-v4-pro for real pending approvals, questions, joins and group mentions. Never fabricate Inbox rows.

Open Activity from A's rail and narrow page. Inspect all three sections, names and avatars in light/dark. Open B's approval and question details; submit a decision/answer, and verify B's canonical state changes while A has no matching write. Snooze a join, inspect Later, cancel snooze, then accept or decline it. Clear a reply and verify B's dismissed query; failures must stay visible, not look successful. Source links open B's conversation and the narrow Back path returns to Activity. Search and load-more use B's device gateway.

Stop only B's recorded PID to inspect offline cached rows, then restart that task Profile. Revoke A's paired device from B and confirm refused rows disappear. Keep ARIA, console and canonical evidence for each exercised path; label unrun paths separately. This logical two-Host browser journey does not qualify DSH Desktop, physical-machine tailnet or native iOS/Android. Host looks are device-local by design. Full remote conversation images/cards are a separate track.
Binary file added .github/pr-screenshots/2500-t3/a-d.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added .github/pr-screenshots/2500-t3/a-l.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added .github/pr-screenshots/2500-t3/b-d.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added .github/pr-screenshots/2500-t3/b-l.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added .github/pr-screenshots/2500-t3/e-d.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added .github/pr-screenshots/2500-t3/e-l.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added .github/pr-screenshots/2500-t3/f-d.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added .github/pr-screenshots/2500-t3/f-l.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added .github/pr-screenshots/2500-t3/open.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added .github/pr-screenshots/2500-t3/p-d.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added .github/pr-screenshots/2500-t3/p-l.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added .github/pr-screenshots/2500-t3/s-d.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added .github/pr-screenshots/2500-t3/s-l.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
17 changes: 17 additions & 0 deletions changes/2500-remote-activity.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
---
section: Added
---

## en

- Activity Center groups paired Remote Hosts' requests, replies and unread conversations by Host. Remote names and avatars stay scoped to their Host; approvals, questions, join requests, snooze and clear act on that Host, and source links open its conversation ([#2500](https://github.com/BotHarness/DeepSeekBot/issues/2500)).
- Remote read and submission failures show a short translated retry hint while keeping loaded items and diagnostic causes. Decisions already handled on another device or expired do not count as a successful submission ([#2500](https://github.com/BotHarness/DeepSeekBot/issues/2500)).
- Unreadable remote activity items show a retry hint while valid items remain usable; retrying a failed conversation open targets the same Host ([#2500](https://github.com/BotHarness/DeepSeekBot/issues/2500)).
- Switching remote Hosts or Channels clears the previous conversation's unsent draft in Activity Center and the conversation panel, preventing it from being sent to the new destination ([#2500](https://github.com/BotHarness/DeepSeekBot/issues/2500)).

## zh

- 活动中心按 Host 展示已配对远程的请求、回复和未读会话,使用各自的名称与头像;审批、问题、加入请求、稍后处理和清除均发送到对应 Host,来源链接打开其会话([#2500](https://github.com/BotHarness/DeepSeekBot/issues/2500))。
- 远程读取和提交失败时保留已有项目与诊断原因,并显示简短的本地化重试提示;已被另一设备处理或已过期的决定不再计为本次提交成功([#2500](https://github.com/BotHarness/DeepSeekBot/issues/2500))。
- 远程活动中无法读取的项目会显示重试提示,有效项目仍可操作;打开会话失败后,重试仍指向同一 Host([#2500](https://github.com/BotHarness/DeepSeekBot/issues/2500))。
- 活动中心和会话面板切换远程 Host 或频道时会清空原会话尚未发送的草稿,避免误发给新来源([#2500](https://github.com/BotHarness/DeepSeekBot/issues/2500))。
2 changes: 1 addition & 1 deletion docs/architecture/botharness-architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -1138,7 +1138,7 @@ Chats shares its persisted device-local conversation filter with Activity. The n

The application-defined browser `RemoteHosts` owner keeps its pairings and selected Host tab in device-local storage. Each Remote Host reuses the phone's Host client and conversation module through the device gateway; This Host never relays the Remote Host's credential. The shared `createInbox(client, { activity: true })` supplies each Remote Host's Human Inbox and unread snapshot. The first browser subscriber starts the client and Inbox, and the last stops them while retaining the last Inbox snapshot. Authenticated avatar fetches publish an immutable URL-to-object-URL map; removal, roster replacement and final unsubscribe release obsolete objects and discard late fetch results. The storage-key rename imports existing browser pairings and the selected tab. See [ADR-0242](../adr/0242-a-browser-pairs-other-computers-through-their-device-gateway.md) and [ADR-0243](../adr/0243-every-client-reads-every-host-through-one-host-feed.md).

This scaffold does not yet display remote avatars, activity, unread counts or Activity Center items. This Host still reads its existing bridge/store until those Host Feed slices migrate. Device Notification Preference remains a subsequent device-owned slice; the browser has no closed-page notification path.
Activity Center consumes each Remote Host's shared Inbox through `RemoteHosts.activity`, reusing the existing `HumanInboxView` for Host groups. Its Host-scoped source supplies identity, pagination, source navigation and commands; it never receives This Host's `BridgeActions`. `createHostActivityActions` adapts the shared attention and request operations to the existing approval and question cards. Each operation resolves the current pairing before calling its Host client, and confirmed changes refresh that Host's Inbox. Filtered observers share the Host client and stop with their last subscriber. This Host still reads its existing bridge/store until that Host Feed slice migrates. Device Notification Preference remains a subsequent device-owned slice; the browser has no closed-page notification path.

### Version-bound Memory image previews (#2214, first Web slice)

Expand Down
7 changes: 5 additions & 2 deletions packages/client/src/client/activity-center-panel.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
import { ActivityInboxHeader } from './activity-inbox-menu.js';
import type { BridgeActions } from './actions.js';
import { ActivityCenterView } from './activity-center-view.js';
import { ACTIVITY_CENTER_PANEL, type DockPanel } from './dock-panels.js';
import { ACTIVITY_CENTER_PANEL, type DockPanel, type DockPanelRegistry } from './dock-panels.js';
import type { BotHarnessTranslate } from './locale.js';

export function createActivityCenterPanel(
actions: BridgeActions,
t: BotHarnessTranslate,
registry?: DockPanelRegistry,
): DockPanel {
return {
id: ACTIVITY_CENTER_PANEL,
Expand All @@ -16,6 +17,8 @@ export function createActivityCenterPanel(
order: -100,
defaultPosition: { zone: 'right' },
headerAction: () => <ActivityInboxHeader actions={actions} t={t} />,
component: () => <ActivityCenterView actions={actions} t={t} placement="panel" />,
component: () => (
<ActivityCenterView actions={actions} t={t} placement="panel" registry={registry} />
),
};
}
26 changes: 25 additions & 1 deletion packages/client/src/client/activity-center-view.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
import { ActivityInboxHeader } from './activity-inbox-menu.js';
import { useMemo, type ReactElement } from 'react';
import { useMemo, useSyncExternalStore, type ReactElement } from 'react';
import { conversationKey, remoteHosts } from './remote-hosts.js';
import { RemoteConversationView } from './remote-conversation-view.js';
import { ActivityHostHeading, RemoteActivityGroup } from './remote-activity.js';
import type { DockPanelRegistry } from './dock-panels.js';
import type { BridgeActions } from './actions.js';
import { HumanInboxView } from './human-inbox-view.js';
import { zhTranslate, type BotHarnessTranslate } from './locale.js';
Expand All @@ -21,12 +25,16 @@ export function ActivityCenterView({
actions,
t = zhTranslate,
placement = 'page',
registry,
}: {
actions: BridgeActions;
t?: BotHarnessTranslate;
placement?: 'page' | 'panel';
registry?: DockPanelRegistry | undefined;
}): ReactElement {
const panel = placement === 'panel';
const sources = remoteHosts();
const remote = useSyncExternalStore(sources.subscribe, sources.getSnapshot);
const { feed, query, pages, collapsed } = useActivityFeed(actions);
const scoped = useMemo(
() =>
Expand All @@ -48,6 +56,8 @@ export function ActivityCenterView({
detach?.();
};
}, [actions, panel, feed]);
if (!panel && !registry?.available() && remote.open !== undefined)
return <RemoteConversationView key={conversationKey(remote.open)} target={remote.open} t={t} />;
return (
<div
ref={mount}
Expand All @@ -64,6 +74,9 @@ export function ActivityCenterView({
</header>
)}
<div className="bh-activity-sections">
{remote.hosts.length > 0 ? (
<ActivityHostHeading name={t('remoteHosts.this')} t={t} />
) : null}
{feed.visible().map((section) => {
const page = pages[section];
const count =
Expand Down Expand Up @@ -101,6 +114,17 @@ export function ActivityCenterView({
/>
);
})}
{remote.hosts.map((host) => (
<RemoteActivityGroup
key={host.host.hostId}
host={host}
sources={sources}
avatars={remote.avatars}
registry={registry}
query={query}
t={t}
/>
))}
</div>
</div>
);
Expand Down
4 changes: 2 additions & 2 deletions packages/client/src/client/activity-inbox-styles.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,13 @@ import { color, radius, space, text } from './ds/tokens.stylex.js';

export const ACTIVITY_INBOX_CSS = `
.bh-activity-triage { min-width: 0; }
.bh-activity-triage .bh-activity-sections > .bh-human-inbox { min-width: 0; padding: 0; }
.bh-activity-triage .bh-activity-sections .bh-human-inbox { min-width: 0; padding: 0; }
.bh-activity-triage .bh-activity-inbox { width: 100%; max-width: none; min-width: 0; margin: 0; padding: ${space.x1}; box-sizing: border-box; }
.bh-activity-triage.bh-activity-center-panel .bh-activity-inbox { padding: 0; }
.bh-activity-triage .bh-activity-center-header { display: flex; align-items: center; justify-content: space-between; padding: ${space.x1} ${space.x3}; }
.bh-activity-triage .bh-activity-center-header h1 { margin: 0; }
.bh-activity-triage .bh-human-inbox-workspace { display: block; }
.bh-activity-triage .bh-human-inbox-detail { grid-column: 1 / -1; min-width: 0; }
.bh-activity-triage .bh-human-inbox-detail { position: relative; grid-column: 1 / -1; min-width: 0; }
.bh-activity-header-actions { margin: 0; padding: 0; height: auto; min-width: 0; }
.bh-activity-header-actions:has(.bh-search-open) { flex: 1; }
.bh-activity-header-actions .bh-search-open { width: 100%; margin-inline: 0; }
Expand Down
4 changes: 2 additions & 2 deletions packages/client/src/client/bot-main.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3280,7 +3280,7 @@ function ConversationView({
state.selection === undefined ? (
<Welcome state={state} t={t} />
) : state.selection.kind === 'inbox' ? (
<ActivityCenterView actions={actions} t={t} />
<ActivityCenterView actions={actions} t={t} registry={dockPanels} />
) : (
conversationPane
);
Expand Down Expand Up @@ -3517,7 +3517,7 @@ function BotMainContent({
const workspace = useContext(WorkspaceContext);
if (!workspace && state.selection === undefined) return <Welcome state={state} t={t} />;
if (!workspace && state.selection?.kind === 'inbox')
return <ActivityCenterView actions={actions} t={t} />;
return <ActivityCenterView actions={actions} t={t} registry={dockPanels} />;
const scopeKey =
state.selection === undefined
? 'welcome'
Expand Down
2 changes: 1 addition & 1 deletion packages/client/src/client/bridge.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3889,7 +3889,7 @@ export async function loadBotAttention(
return parseBotAttentionPage(await unwrap(call, 'botAttention', { slug, limit, cursor, state }));
}

function parseHumanAttentionPage(value: unknown): HumanAttentionPage {
export function parseHumanAttentionPage(value: unknown): HumanAttentionPage {
const row = asRecord(value);
const raw = row?.['items'];
if (!Array.isArray(raw)) throw new Error('invalid Human attention page');
Expand Down
4 changes: 3 additions & 1 deletion packages/client/src/client/channel-dock.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { mountWorkspaceChrome, updateWorkspaceChrome } from './workspace-chrome.js';
import { useRemoteHosts } from './host-tabs.js';
import { RemoteConversationView } from './remote-conversation-view.js';
import { conversationKey } from './remote-hosts.js';
import { AppearancePanelHeader } from './appearance-panel-header.js';
import { AppearanceContext } from './appearance-context.js';
import { PanelAppearanceSettings } from './appearance-panel-settings.js';
Expand Down Expand Up @@ -328,7 +329,8 @@ function workspaceState(dock: DockContextValue, id: string): SubpanelState | und
function ChannelBodyPanel(): ReactElement {
const dock = useDock();
const remote = useRemoteHosts().open;
if (remote !== undefined) return <RemoteConversationView target={remote} t={dock.t} />;
if (remote !== undefined)
return <RemoteConversationView key={conversationKey(remote)} target={remote} t={dock.t} />;
const panels: Record<string, { label: string; header?: ReactNode; content: ReactNode }> = {
[MESSAGE_LEAF]: { label: dock.t('workspace.messages'), content: <ChannelMessageContent /> },
};
Expand Down
Loading
Loading