mirror of
https://github.com/QwenLM/qwen-code.git
synced 2026-11-07 06:19:43 -10:00
* feat(web-shell): 增加 managed-host 构建入口与托管会话页 为产品侧集成提供独立的 managed-host 产物(--mode managed-host)与托管会话页: 不挂 daemon workspace/session context,托管会话由 provider 直接渲染列表、会话与输入框。 包含:vite lib 构建模式、managed-host 渲染入口及测试、ManagedSessionsPage 托管分支与 测试、provider 类型调整、README 与中英文设计说明。本次不涉及宿主注入能力。 * feat(web-shell): 托管宿主可注入侧栏导航 props ManagedAgentWebShell 增加可选导航 props 并原样透传给 ManagedSessionsPage: navigationTarget / archivedNavigationTarget / onArchivedSessionsStatusChange / navigationActive / navigationSearch。 宿主据此决定是否把官方会话列表 portal 到自己的侧栏(navigationTarget 为 null 时 仍走 home 布局但不 portal),并同步搜索词与归档计数。类型复用 ManagedSessionsPage 现有 props,不新增重复定义;新增 2 个用例覆盖原样透传与未传时保持 undefined。 * feat(web-shell): add full-page app-host entry and align managed empty-state layout - Add app-host mount entry (renderWebShellApp) so a non-React host can render the complete WebShell (sidebar + welcome + composer) by injecting baseUrl/provider; no shell is hand-drawn and no backend address is hardcoded. - Center the managed empty state like the standalone chat (content shrinks so the chatPane centers it) instead of pinning the welcome to the top and composer to the bottom; gate the welcome header off when a failed session is selected. - Surface session failure and unconfirmed-submission notices together above the composer, dedupe the same message across channels, and style them as callouts. - A pending prompt owned by another session now offers a redirect to the owning conversation instead of retry/discard from a foreign session; reconcile tests. - Build app-host with global (unscoped) CSS so root-level state classes match. * test(web-shell): align managed status smoke assertions Co-authored-by: Qwen-Coder <[email protected]> * test(web-shell): reach the owning conversation before retrying a foreign pending A pending prompt owned by another conversation no longer offers retry in the foreign session: it renders "Open the original conversation" instead. The spec still clicked "Retry the same request" straight from the foreign session, so the two foreign variants timed out waiting for a button that is deliberately absent from this head while the owner variant passed. Follow the redirect first, then retry. Owner variant unchanged. Co-authored-by: Qwen-Coder <[email protected]> Patrol-Run: qwen-pr-closeout/jmvwo8rnm58 * docs(web-shell): document the app-host entry and drop ignored e2e links The review on head932a61d8dcread `app-host` as undocumented: no README section, no mention in either design doc. Document it the same way `managed-host` is documented -- entry API, build mode, served directory -- and record the unscoped-CSS behaviour as a deliberate decision narrowed to that bundle, naming the entries that keep scoped CSS. Also state that neither host mode is part of `npm run build`, which is what the wiring actually does. Both design docs linked `.qwen/e2e-tests/managed-host-webshell.md`, which `.gitignore` excludes (`.qwen/*`), so the links are dropped and the statement they supported stays. Co-authored-by: Qwen-Coder <[email protected]> Patrol-Run: qwen-pr-closeout/jmvxdyp0v69 * fix(web-shell): revert breaking changes to existing consumers - Revert `isChatEmptyState` to `!managedMode &&` (was: `!connection.sessionId && !(managedAgentProvider !== undefined && managedSessionId)`), restoring the original behavior where managed mode is never empty state. The head condition broke existing managed consumers by making them hit empty-state layout when no session was selected. - Revert `sidebarRailEnabled` to drop `managedAgentProvider === undefined &&`, restoring rail for existing managed consumers. The condition was app-host-specific and should not affect hosts that already pass a provider. - Revert `ManagedSessionsPage` inline `flex: 0 0 auto` (was unconditional in empty state), restoring `flex: 1 1 auto` for the managed entry. The shrink was only correct when paired with `.appChatEmpty .chatPane { justify-content: center }`, which only exists in the full-page `renderWebShellApp` entry. In `renderManagedAgentWebShell` the class is absent, so the shrink alone produced a 136px content block pinned to the top with 550px dead space below. - Remove `skipScope` from `vite.lib.config.ts`: the justification ("scoped selectors use [data-web-shell-root] as an ancestor anchor") is factually incorrect. `COMPONENT_SCOPE` includes `:where([data-web-shell-root][data-web-shell-shadcn], ...)` which matches the element itself, so scoped bundles already apply state classes on the root element. Keep scoping for all modes. - Update README and both design docs to reflect the corrected scoping behavior. Co-authored-by: Qwen-Coder <[email protected]> * chore(web-shell): remove app-host entry The app-host entry (renderWebShellApp) had no build wiring (absent from build script, exports, files, and no CI job invoked --mode app-host), no test coverage, and hardcoded a specific host's sidebar/footer configuration. Remove it from this PR; it can be reintroduced in a follow-up with proper packaging and tests once a named consumer exists. Co-authored-by: Qwen-Coder <[email protected]> * fix(web-shell): drop unused mode parameter from injectCssModules The mode parameter became unused after the skipScope removal. Remove it to fix the ESLint no-unused-vars error. Co-authored-by: Qwen-Coder <[email protected]> * docs(web-shell): scope the managed-host entry and correct stale layout notes - README: say plainly that `managed-host` is source-only (no `build`, `exports` or `files` wiring, so it is not in the npm package), and record that one shell per shadow root is the contract. - package.json: keep the orphan `dist/types/managed-host.d.ts` out of the published tree so the package does not ship a type with no implementation. - ManagedSessionsPage: point the `summary.failure` dedupe comment at the current failure block above the composer, and record why a session switch resets failure and discard notices. Co-authored-by: Qwen-Coder <[email protected]> Patrol-Run: qwen-pr-closeout/jmvy1jgjo76 * docs(web-shell): state the real consequence of the session-switch reset The comment claimed the list error is re-derived on a session switch. It is not: the list effect's deps exclude sessionId, so a catalog-load error is hidden until provider/listRevision changes. Say what actually happens. Co-authored-by: Qwen-Coder <[email protected]> Patrol-Run: qwen-pr-closeout/jmvy1jgjo76 * fix(web-shell): keep managed sessions visible when workspaces collapse Co-authored-by: Qwen-Coder <[email protected]> * fix(web-shell): address doudouOUC review feedback - Remove shadow-DOM style adoption from renderManagedAgentWebShell: the shared CSSStyleSheet cache caused cross-contamination when two shells shared one shadow root. Hosts using shadow DOM should adopt styles themselves or use a plain container. - Scope the error/discard reset effect to hostSessionOnly only: the unconditional clear on [provider, sessionId] hid the catalog-load error for existing managed consumers on every session switch without retrying. - Update the managed-host test to match the removed shadow-DOM behavior. - Drop the stale shadow-root note from README. Co-authored-by: Qwen-Coder <[email protected]> * fix(web-shell): preserve styles for managed hosts in shared shadow roots Restore the documented managed-host ShadowRoot style lifecycle and keep the shared package sheet until its last owner unmounts. Cover both fallback and constructable sheet behavior. Co-authored-by: Qwen-Coder <[email protected]> * fix(web-shell): center the managed host empty conversation as one group The host mount entry has no .appChatEmpty ancestor, so the managed empty conversation kept its full-height content box and pinned the composer to the bottom. Give host-mounted empty conversations the same shrink-to-content centering, mount the entry through the managedHome fill wrapper, and drop the empty trailing status row below the composer. Co-authored-by: Qwen-Coder <[email protected]> * fix(web-shell): let a host-mode session clear through without an echo channel hostSessionOnly alone marks the shell controlled, so the page's clear signal after deleting or archiving the current session was dropped when the host passed no onSessionChange (the README-documented mount). The deleted conversation stayed on screen and sendable. Allow the internal write for a clear (undefined) when no echo channel exists; new selections stay host-owned, preserving the existing 'unrequested selection is suppressed' contract. The bot's suggested controlled-expression rewrite would have broken that contract, so the gate lives in the handler instead. Witness: the new no-echo test fails at5683eed0without this change (sessionId stays 's1') and passes with it; the echo-channel test pins that the fix does not leak internal writes into echo-owned hosts. --------- Co-authored-by: Shaojin Wen <[email protected]> Co-authored-by: Qwen-Coder <[email protected]>
65 lines
2.3 KiB
TypeScript
65 lines
2.3 KiB
TypeScript
// @vitest-environment jsdom
|
|
|
|
import { act } from 'react';
|
|
import { expect, it, vi } from 'vitest';
|
|
import { renderManagedAgentWebShell } from './managed-host';
|
|
|
|
vi.mock('./ManagedAgentWebShell', () => ({
|
|
ManagedAgentWebShell: ({ sessionId }: { sessionId?: string }) => (
|
|
<div data-web-shell-root>{sessionId}</div>
|
|
),
|
|
}));
|
|
|
|
it('mounts, replaces props and disposes a host root', async () => {
|
|
const host = document.createElement('div');
|
|
document.body.appendChild(host);
|
|
const container = document.createElement('div');
|
|
host.appendChild(container);
|
|
let shell!: ReturnType<typeof renderManagedAgentWebShell>;
|
|
try {
|
|
await act(async () => {
|
|
shell = renderManagedAgentWebShell(container, { sessionId: 'first' });
|
|
});
|
|
expect(container.textContent).toBe('first');
|
|
await act(async () => shell.update({ sessionId: 'second' }));
|
|
expect(container.textContent).toBe('second');
|
|
await act(async () => shell.unmount());
|
|
expect(container.childElementCount).toBe(0);
|
|
} finally {
|
|
host.remove();
|
|
}
|
|
});
|
|
|
|
it('keeps shadow-root styles until the last host root unmounts', async () => {
|
|
const packageStyle = document.createElement('style');
|
|
packageStyle.dataset.qwenWebShell = 'component';
|
|
packageStyle.textContent = '[data-web-shell-root] { color: red; }';
|
|
document.head.appendChild(packageStyle);
|
|
const host = document.createElement('div');
|
|
document.body.appendChild(host);
|
|
const shadow = host.attachShadow({ mode: 'open' });
|
|
const first = document.createElement('div');
|
|
const second = document.createElement('div');
|
|
shadow.append(first, second);
|
|
let left!: ReturnType<typeof renderManagedAgentWebShell>;
|
|
let right!: ReturnType<typeof renderManagedAgentWebShell>;
|
|
try {
|
|
await act(async () => {
|
|
left = renderManagedAgentWebShell(first, { sessionId: 'first' });
|
|
right = renderManagedAgentWebShell(second, { sessionId: 'second' });
|
|
});
|
|
const hasStyles = () =>
|
|
shadow.querySelectorAll('style').length > 0 ||
|
|
(shadow.adoptedStyleSheets?.length ?? 0) > 0;
|
|
expect(hasStyles()).toBe(true);
|
|
await act(async () => left.unmount());
|
|
expect(second.textContent).toBe('second');
|
|
expect(hasStyles()).toBe(true);
|
|
await act(async () => right.unmount());
|
|
expect(hasStyles()).toBe(false);
|
|
} finally {
|
|
packageStyle.remove();
|
|
host.remove();
|
|
}
|
|
});
|