Skip to content
Merged
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
4 changes: 4 additions & 0 deletions .agents/skills/dsh-dev/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -324,3 +324,7 @@ normalize only the application Settings overlay's safe-area padding; keep the ca
The native Menu's portaled anchor can be multiplied again (observed y=1247 in an 820px
viewport). The compact application section picker reuses the existing viewport menu owner,
which clamps and converts coordinates. This does not qualify other native dropdowns.

## Windows vfile browser imports (#2289)

Rolldown can pass a Windows importer path with backslashes. The vfile browser-variant override must normalize separators before matching `node_modules/vfile/lib`; otherwise a successful build can retain `require("node:process")`, and the real Client refuses @botharness/ui before its Services exist. The existing Client bundle contract reproduces this: two assertions fail before normalization and all seven pass after it. Rebuild the Client and restart the task-owned Host, then inspect its actual browser console; API health alone does not qualify plugin boot.
2 changes: 1 addition & 1 deletion .agents/skills/verify-deepseekbot/features/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ One file per user-facing feature. Each file has four sections: `Sub-features`, `

| Feature | Needs a model | Scripted journey | File |
| -------------------------------------------------------------------------------------------------- | -------------------------- | ---------------------------------------------------------------------------------------------------- | ---------------------------------------------------- |
| Bot mode entry and the default DeepSeek Bot | no | `scripts/e2e-bot-mode-sidebar.mjs`, `scripts/e2e-panel-workspace.mjs` | [bot-mode.md](bot-mode.md) |
| 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) |
| Bot DM reply | yes | `scripts/e2e-llm-scenarios.mjs --scenario dm-reply` | [bot-dm-reply.md](bot-dm-reply.md) |
Expand Down
7 changes: 7 additions & 0 deletions .agents/skills/verify-deepseekbot/features/bot-mode.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ Bot mode swaps the native DSH session view for the BotHarness conversation view.

- `enter` switches from the native view to Bot mode.
- `sidebar` is the desktop Bot mode sidebar: a `DeepSeekBot` header, pinned panel cards (one per Dock panel, Activity Center first with its unread count), the Messages Roster and Bot settings, in the column DSH sizes and collapses. A card toggles its panel in the dock and is pressed while the panel is open; before a Channel is open only the Activity Center card works, and it opens the full Activity Center page.
- `bottom-return` adds a permanent **Back to Harness / 返回 Harness** icon directly below Bot settings in the qualified wide workspace Control Rail, using the same exit operation as the top logo. It remains visible without hover or tutorial completion.
- `exit` leaves Bot mode with the header logo, which morphs into **Exit Bot mode** on hover or keyboard focus, and brings back the native left column.
- `default-bot` prepares Tob and its DM on first entry.
- `welcome-letter` preserves the older DeepSeek Bot welcome; fresh Tob uses a once-only fixed greeting.
Expand Down Expand Up @@ -41,3 +42,9 @@ Preconditions: a fresh instance that passes `doctor`. For the qualified workspac
- `.bh-root` alone does not mean Bot mode: the Window Companion stage carries it too. `verify-app` waits for `.bh-root.bh-main`.
- The preview notice is modal and hides the rest of the page from the accessibility tree until it is dismissed.
- On a keyless Host, the native `Add an API key to get started` dialog can appear over Bot mode. `verify-app` chooses `Configure later` unless `--keep-overlays` is set.

## Bottom return (#2289)

Run `BH_E2E_INSTANCE=<task-instance.json> node scripts/e2e-bot-mode-exit.mjs` against the task-owned isolated Host. It requires no model call. The journey verifies the visible resting button beneath settings, pointer and Enter/Space exit, native navigation restoration, remembered manual exit after reload, remembered Bot mode after re-entry/reload, unchanged PersonaBot facts and saved layout, the original logo exit, a short window and narrow/wide transitions. Each attempt keeps screenshots, accessibility snapshots, console logs and Client observations.

For a before/after pair, the unmodified main run must fail specifically because the final rail button is still Bot settings. A shell/plugin startup failure is not this regression. Inspect both themes and localized tooltips, move the Control Rail and preserve a real conversation draft in the actual Client; save separate native Desktop evidence when available. Browser evidence does not qualify a native window.
17 changes: 17 additions & 0 deletions .github/pr-screenshots/2289-return-to-harness/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# Return to Harness — #2289

Captured on 2026-10-19 in an isolated DSH 0.2.1-rc.2 Web Host on Windows, at 1440 × 900. No production Profile or receiver was used.

The before pair uses the original UI source from `33334b22cb7d9e79f667b171d089c71df2dc572a`, with the Windows-only vfile import normalization needed to boot it. The candidate UI is the implementation in `c4b3967e34fe3d8d51d08664f97f871212cd1344`; the subsequent inventory commit changes no UI. Light/English and dark/Chinese pairs share viewport, Home state, Tob fixture and resting pointer position. The new bottom icon is always visible beneath settings; the top Bot-logo exit remains.

## Verification

- Existing Client bundle contract: 2 baseline failures from Node-only imports on Windows; all 7 pass after path normalization.
- Existing last-view, workspace-shell, sidebar and panel-entry regressions: 19 pass. Typecheck, repository format check and lint pass.
- `scripts/e2e-bot-mode-exit.mjs`: English/light and Chinese/dark pass. Real pointer and Enter/Space exit, native Harness navigation, ordinary mode retained on refresh, explicit Bot-mode re-entry/restoration, same PersonaBot facts, moved-rail panel geometry (within 2px native rounding), short window, narrow/wide transitions and original top-logo exit. Both final runs checked a real unsent Home draft.
- Tooltip and keyboard focus: localized label, visible focus and tooltip geometry pass in both themes/languages. The published focus screenshot is actual keyboard focus.
- Full local suite was invoked once; it reports unrelated Host test failures/timeouts on Windows, including reproducible EPERM during telemetry fixture cleanup. Final totals and CI status are recorded in the PR.

The launcher used task-only instrumentation to omit the machine's unrelated OpenCode Go selection. All normal isolated Profile, authentication, pinned CLI and API checks remained active; no model call was needed. Browser results do not qualify native Desktop. The installed Desktop is 0.1.7-rc.2 and was not used to claim 0.2.1-rc.2 acceptance. Narrow fallback behavior was checked, but its interface was not redesigned.

Retained private evidence includes failed baseline boot attempts, the specific missing-bottom-button red runs, screenshots, accessibility snapshots, console logs and Client diagnostics. These are test evidence, not canonical product state.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
13 changes: 13 additions & 0 deletions changes/2289-return-to-harness.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
---
section: Added
---

## en

- Added a permanent Back to Harness icon below settings in the wide Bot-mode Control Rail, while keeping the top-logo exit and last-opened-mode restoration ([#2289](https://github.com/BotHarness/DeepSeekBot/issues/2289)).
- Corrected Windows browser builds so vfile resolves its browser variants instead of importing Node-only modules that prevent the Client from starting ([#2289](https://github.com/BotHarness/DeepSeekBot/issues/2289)).

## zh

- 在宽屏 Bot 模式控制栏的设置下方新增常驻「返回 Harness」图标,保留顶部 Logo 退出和上次打开模式的恢复行为([#2289](https://github.com/BotHarness/DeepSeekBot/issues/2289))。
- 修正 Windows 浏览器构建对 vfile 的解析,使用浏览器版本,避免导入 Node 专用模块导致 Client 无法启动([#2289](https://github.com/BotHarness/DeepSeekBot/issues/2289))。
2 changes: 2 additions & 0 deletions packages/client/src/client/locale.ts
Original file line number Diff line number Diff line change
Expand Up @@ -846,6 +846,7 @@ export const zh = {
'panel.label': 'Bot 模式',
'panel.settings': 'Bot 设置',
'panel.exit': '退出 Bot 模式',
'panel.backToHarness': '返回 Harness',
'panel.brand': 'DeepSeekBot',
'panel.sidebar.open': '打开侧边栏',
'panel.sidebar.collapse': '收起侧边栏',
Expand Down Expand Up @@ -4704,6 +4705,7 @@ export const en = {
'panel.label': 'Bot mode',
'panel.settings': 'Bot settings',
'panel.exit': 'Exit Bot mode',
'panel.backToHarness': 'Back to Harness',
'panel.brand': 'DeepSeekBot',
'panel.sidebar.open': 'Open sidebar',
'panel.sidebar.collapse': 'Collapse sidebar',
Expand Down
5 changes: 5 additions & 0 deletions packages/client/src/client/workspace.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -399,6 +399,11 @@ function WorkspaceRail({
<ChannelSidebarIcon name="settings" />
</button>
</Tooltip>
<Tooltip label={t('panel.backToHarness')} side="right">
<button type="button" aria-label={t('panel.backToHarness')} onClick={options.exitBotMode}>
<ExitIcon size={20} />
</button>
</Tooltip>
</nav>
);
}
240 changes: 240 additions & 0 deletions scripts/e2e-bot-mode-exit.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,240 @@
import assert from 'node:assert/strict';
import { createRequire } from 'node:module';
import { mkdirSync, readFileSync, readdirSync, writeFileSync } from 'node:fs';
import { dirname, resolve } from 'node:path';
import { fileURLToPath } from 'node:url';
import { rpc } from './verify-app.mjs';

const root = resolve(dirname(fileURLToPath(import.meta.url)), '..');
const modules = resolve(root, 'node_modules/.pnpm');
const puppeteer = createRequire(
resolve(
modules,
readdirSync(modules).find((name) => name.startsWith('puppeteer@')),
'node_modules/',
),
)('puppeteer');
assert.ok(process.env.BH_E2E_INSTANCE, 'Set BH_E2E_INSTANCE to the task-owned instance.json');
const instance = JSON.parse(readFileSync(process.env.BH_E2E_INSTANCE, 'utf8'));
const out = resolve(
process.env.BH_SCREENSHOT_DIR ?? dirname(process.env.BH_E2E_INSTANCE),
`mode-exit-${Date.now()}`,
);
mkdirSync(out, { recursive: true });
const browser = await puppeteer.launch({ headless: true, args: ['--no-sandbox', '--lang=en-US'] });
const page = await browser.newPage();
const locale = process.env.BH_E2E_LOCALE ?? 'en';
const theme = process.env.BH_E2E_THEME ?? 'light';
const errors = [];
const logs = [];
const results = [];
const rail = '.bh-workspace-rail';
let step = 'login';
let chinese = false;
const label = (en, zh) => (chinese ? zh : en);
const click = (name) => page.locator(`::-p-aria(${name}[role="button"])`).click();
const sleep = (ms) => new Promise((done) => setTimeout(done, ms));
page.on('pageerror', (error) => errors.push(error.message));
page.on('console', (message) => {
if (['warn', 'error'].includes(message.type())) logs.push(`${message.type()}: ${message.text()}`);
});
async function dismissNotices() {
for (let pass = 0; pass < 4; pass++) {
for (const name of [
'Continue',
'继续',
'Configure later',
'稍后配置',
'Skip tutorial',
'跳过教程',
]) {
const button = await page.$(`::-p-aria(${name}[role="button"])`);
if (button && (await button.boundingBox())) await button.click();
await button?.dispose();
}
await sleep(100);
}
}
async function capture(name) {
await page.mouse.move(1400, 30);
await sleep(350);
await page.screenshot({ path: resolve(out, `${name}.png`) });
const ax = await (await page.createCDPSession()).send('Accessibility.getFullAXTree');
writeFileSync(resolve(out, `${name}.aria.json`), JSON.stringify(ax, null, 2));
const observed = await page.evaluate(() => ({
state: window.__BOTHARNESS_CLIENT_DIAGNOSTICS__?.snapshot().state,
rail: !!document.querySelector('.bh-workspace-rail'),
nativeSidebar:
!!document.querySelector('[data-slot="sidebar"]') &&
!document.querySelector('[data-bh-workspace-frame]'),
lastView: JSON.parse(localStorage.getItem('botharness/last-view.v1')),
}));
assert.equal(observed.state, 'shell-ready');
results.push({ name, ...observed });
}
async function enter() {
await click(label('Bot mode', 'Bot 模式'));
await page.waitForSelector(rail);
await dismissNotices();
}
async function exitBottom(key) {
const name = label('Back to Harness', '返回 Harness');
const button = await page.$(`::-p-aria(${name}[role="button"])`);
assert.ok(
button && (await button.boundingBox()),
'a visible Back to Harness button is available without hover',
);
if (key) {
await button.focus();
await page.keyboard.press(key);
} else await button.click();
await page.waitForSelector('[data-bh-workspace-frame]', { hidden: true });
await page.waitForSelector(`::-p-aria(${label('Bot mode', 'Bot 模式')}[role="button"])`);
}
try {
await page.setViewport({ width: 1440, height: 900 });
const cdp = await page.createCDPSession();
await cdp.send('Emulation.setUserAgentOverride', {
userAgent: await browser.userAgent(),
acceptLanguage: locale === 'zh' ? 'zh-CN,zh' : 'en-US,en',
});
await page.emulateMediaFeatures([{ name: 'prefers-color-scheme', value: theme }]);
await page.goto(instance.url, { waitUntil: 'networkidle2' });
await page.goto(instance.url, { waitUntil: 'networkidle2' });
await dismissNotices();
chinese = await page.evaluate(
() =>
!!document.querySelector('button[aria-label="Bot 模式"], button[aria-label="退出 Bot 模式"]'),
);
if (!(await page.$(rail))) await enter();
await dismissNotices();
await page.waitForSelector(`${rail} button`);
await page.waitForSelector('[data-dock-panel="bh:channels"] [data-channel-id]');
await page.waitForFunction(
() => !document.body.innerText.includes('Loading available models...'),
);
await capture('before-interaction');
const canonical = await rpc(instance, 'botharness/list');
const geometry = () =>
page.$$eval('[data-dock-panel], .bh-workspace-rail', (nodes) =>
nodes
.map((node) => ({
id: node.getAttribute('data-dock-panel') ?? 'rail',
box: node.getBoundingClientRect().toJSON(),
}))
.sort((a, b) => a.id.localeCompare(b.id)),
);

step = 'bottom-return';
const placement = await page.$eval(rail, (node) => {
const buttons = [...node.querySelectorAll(':scope > button')];
return buttons.map((button) => ({
label: button.getAttribute('aria-label'),
box: button.getBoundingClientRect().toJSON(),
}));
});
const bottomName = label('Back to Harness', '返回 Harness');
assert.equal(
placement.at(-1)?.label,
bottomName,
'return follows settings at the bottom of the rail',
);
assert.equal(placement.at(-2)?.label, label('Bot settings', 'Bot 设置'));
await click(label('Move control rail', '移动控制列'));
await page.locator(`::-p-aria(${label('Move to right edge', '移至最右')})`).click();
await sleep(300);
const layout = await geometry();
await capture('rail-moved');
const draftName = label('What can I help you with?', '有什么可以帮你?');
const draftField = `::-p-aria(${draftName}[role="textbox"])`;
const draftChecked = !!(await page.$(draftField));
if (draftChecked) await page.locator(draftField).fill('Unsent navigation check');
await exitBottom();
await capture('ordinary-harness');
await page.reload({ waitUntil: 'networkidle2' });
await dismissNotices();
assert.equal(await page.$(rail), null, 'manual exit survives reload');
await capture('ordinary-after-reload');
await enter();
const restored = await geometry();
assert.deepEqual(
restored.map((panel) => panel.id),
layout.map((panel) => panel.id),
);
for (let index = 0; index < layout.length; index++) {
for (const axis of ['x', 'y', 'width', 'height']) {
assert.ok(
Math.abs(layout[index].box[axis] - restored[index].box[axis]) <= 2,
`exit preserves ${layout[index].id} ${axis} within native pixel rounding`,
);
}
}
assert.deepEqual(
await rpc(instance, 'botharness/list'),
canonical,
'exit changes no PersonaBot facts',
);
await page.reload({ waitUntil: 'networkidle2' });
await page.waitForSelector(rail);
await dismissNotices();
if (draftChecked) {
await page.waitForSelector(draftField);
assert.equal(await page.$eval(draftField, (node) => node.value), 'Unsent navigation check');
await page.locator(draftField).fill('');
}
await capture('restored-bot-mode');
step = 'keyboard-return';
for (const key of ['Enter', 'Space']) {
await exitBottom(key);
await enter();
}
step = 'top-logo-return';
await page.click(`${rail} button[aria-label="${label('Exit Bot mode', '退出 Bot 模式')}"]`);
await page.waitForSelector('[data-bh-workspace-frame]', { hidden: true });
await enter();
step = 'short-window';
await page.setViewport({ width: 1440, height: 360 });
await sleep(250);
const box = await page.$eval(`button[aria-label="${bottomName}"]`, (node) =>
node.getBoundingClientRect().toJSON(),
);
assert.ok(box.top >= 0 && box.bottom <= 360, 'bottom exit stays in the viewport');
await capture('short-window');
await page.setViewport({ width: 900, height: 800 });
await page.waitForSelector(rail, { hidden: true });
assert.equal(
await page.$(`button[aria-label="${bottomName}"]`),
null,
'narrow fallback has no extra return control',
);
await page.setViewport({ width: 1440, height: 900 });
await page.waitForSelector(rail);
await capture('wide-again');
await click(label('Move control rail', '移动控制列'));
await page.locator(`::-p-aria(${label('Move to left edge', '移至最左')})`).click();
await capture('rail-left-again');
assert.deepEqual(errors, []);
console.log(
JSON.stringify({
result: 'BOT MODE EXIT PASS',
evidence: out,
locale: chinese ? 'zh' : 'en',
theme,
draftChecked,
results: results.length,
}),
);
} catch (error) {
await capture('failure').catch(() => {});
writeFileSync(
resolve(out, 'failure.json'),
JSON.stringify({ step, error: error.message }, null, 2),
);
throw error;
} finally {
writeFileSync(
resolve(out, 'results.json'),
JSON.stringify({ step, results, errors, logs }, null, 2),
);
await browser.close();
}
Loading
Loading