-
Notifications
You must be signed in to change notification settings - Fork 3.1k
feat(web-shell): enrich browser notifications and open target sessions #11447
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
2034a0a
462ee98
b02ab50
9056ceb
567912e
bb8ccdb
0cf3094
321f53b
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,27 @@ | ||
| # Configurable browser notification branding | ||
|
|
||
| [English](web-shell-browser-notification-branding.md) | [简体中文](web-shell-browser-notification-branding.zh-CN.md) | ||
|
|
||
| ## Problem and scope | ||
|
|
||
| Before this PR, #11398 used the fixed `Qwen Code` title and passed no icon to Notification. This PR introduces the bundled PNG and the requested QwenCode spelling, alongside configurable branding. | ||
|
|
||
| The [content and navigation design](web-shell-browser-notification-details.md) defaults to the QwenCode name and bundled icon. QwenCode is the requested notification brand spelling; it is independent of sidebar branding. Hosts embedding Web Shell need to supply their own application name and image URL, including an HTTPS CDN URL. This changes notification branding only, not sidebar branding or browser-controlled site attribution. | ||
|
|
||
| ## Configuration and integration | ||
|
|
||
| Expose `browserNotifications?: WebShellBrowserNotificationsOptions` on `WebShellWithProviders` and its `StandaloneWebShell` alias. The options contain optional `appName` and `iconUrl` strings, plus `defaultEnabled?: boolean` (false by default). Missing or whitespace-only branding values fall back independently to QwenCode and the bundled PNG. The title remains application name · session title, or only the application name when there is no session title. Pass the trimmed icon URL directly to Notification.icon; no proxy, preload, authentication headers, or automatic retry is added. The browser fetches the image under its security policies; a failed custom image does not guarantee the default icon is shown. | ||
|
|
||
| Omitting the options preserves the embedded entry's existing behavior without automatic browser notifications. Passing an object, including an empty object, connects the notification provider above the daemon session provider and exposes the existing UI setting. The initial browser-local preference uses defaultEnabled only when no stored choice exists. If storage is unreadable, start off rather than assuming a missing preference; the user may still explicitly enable notifications temporarily. Explicit stored true or false always wins, including after reload; removing the stored choice restores that instance's initial default. Changing defaultEnabled after mounting does not overwrite the current choice. Permission is never requested automatically, even when the default is on. Changing branding values or toggling the options between an object and undefined preserves the mounted session and notification preference. Omission disables notification contexts and delivery; returning to an object restores the same instance. The initial default is read at mount even if integration is initially omitted. The built-in main.tsx explicitly sets defaultEnabled to true while keeping the default name and icon. Thus the built-in development and installed qwen serve pages start enabled for a fresh browser site, but delivery still requires browser permission and the existing background/unfocused conditions. | ||
|
|
||
| The low-level WebShell component does not gain this prop: its daemon providers are owned by the caller and sit outside its subtree, so an internal notifier could not observe their events. No new CLI option, daemon setting, public daemon protocol, or low-level notification provider API is introduced. Iframe and server-rendered paths continue without browser notifications. | ||
|
|
||
| Notification status and excerpt labels follow the App's resolved UI language, including settings changes. The existing internal settings context synchronizes that language without adding a second locale selection policy. | ||
|
|
||
| ## Click ownership | ||
|
|
||
| Each mounted notification provider owns an internal EventTarget shared with its descendant App through React context. Notification clicks use this target instead of a window-wide broadcast, so only the owning Web Shell navigates. Existing window qwen:open-session events remain available for Markdown links. A notification target that conflicts with the App's current locked workspace is ignored, including a stale notification after the host changes the lock. A target whose owner has unmounted has no active navigation listener. | ||
|
|
||
| ## Validation | ||
|
|
||
| Tests cover public-entry provider order, opt-in versus omitted configuration, default and partial/blank options, CDN URL forwarding, changed options without remounting, preserved permission/off behavior, per-instance notification navigation, locked-workspace rejection, and the existing standalone navigation path. Library builds must include the default image in the published output. Browser verification uses a host fixture importing the public entry with custom branding and a real daemon turn; API capture verifies the name, prompt/reply, and image URL, not OS icon placement or remote CDN availability. | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,27 @@ | ||
| # 浏览器通知品牌配置 | ||
|
|
||
| [English](web-shell-browser-notification-branding.md) | [简体中文](web-shell-browser-notification-branding.zh-CN.md) | ||
|
|
||
| ## 问题与范围 | ||
|
|
||
| 本 PR 之前,#11398 使用固定的 `Qwen Code` 标题,不向 Notification 传入图标。本 PR 引入随包 PNG 和用户指定的 QwenCode 拼写,并支持品牌配置。 | ||
|
|
||
| [内容与导航设计](web-shell-browser-notification-details.zh-CN.md)默认使用 QwenCode 名称和随包图标。QwenCode 是用户指定的通知品牌拼写,与侧栏品牌独立。嵌入 Web Shell 的宿主需要传入自己的应用名称和图片 URL,包括 HTTPS CDN 地址。本次只调整通知品牌,不修改侧边栏品牌或浏览器控制的站点来源。 | ||
|
|
||
| ## 配置与接入 | ||
|
|
||
| 在 WebShellWithProviders 及其 StandaloneWebShell 别名上提供 `browserNotifications?: WebShellBrowserNotificationsOptions`。配置包含可选字符串 appName 和 iconUrl,以及默认值为 false 的可选布尔值 defaultEnabled。名称和图标缺失或仅含空白时分别回退到 QwenCode 和随包 PNG。标题保持“应用名称 · 会话标题”,没有会话标题时只显示应用名称。图片 URL 去掉首尾空白后直接传给 Notification.icon,不增加代理、预加载、认证请求头或自动重试。浏览器按自身安全策略获取图片,自定义图片加载失败不保证自动显示默认图标。 | ||
|
|
||
| 不传配置时保留嵌入入口原有行为,不自动接入浏览器通知。传入对象(包括空对象)时,在 daemon session provider 上方接入通知 provider,并展示已有的 UI 设置。浏览器本地偏好仅在没有保存选择时使用 defaultEnabled。存储不可读时默认关闭,不将其视为没有偏好;用户仍可显式临时开启通知。已保存的 true 或 false 始终优先,刷新后也保留;删除保存值时恢复该实例的初始默认值。挂载后修改 defaultEnabled 不覆盖当前选择。即使默认开启,也不自动申请浏览器权限。修改品牌配置或在对象与 undefined 之间切换配置时,保留已挂载会话和通知偏好。不传配置会停用通知上下文及发送,重新传入对象恢复同一实例。初始默认值在挂载时读取,即使初始未接入通知也是如此。内置 main.tsx 显式传入 defaultEnabled 为 true,并继续使用默认名称和图标。因此内置开发页面和安装后的 qwen serve 页面在新的浏览器站点上默认开启,但实际通知仍需要浏览器授权,并符合已有后台或失焦条件。 | ||
|
|
||
| 低层 WebShell 组件不增加该属性:它的 daemon providers 由调用方管理且在组件子树之外,内部通知组件无法观察那些事件。不增加 CLI 参数、daemon 设置、公共 daemon 协议或低层通知 provider API。iframe 和服务端渲染路径继续不使用浏览器通知。 | ||
|
|
||
| 通知状态和摘要标签跟随 App 实际使用的界面语言,包括设置中的语言修改。通过已有内部设置 context 同步语言,不增加第二套语言选择规则。 | ||
|
|
||
| ## 点击归属 | ||
|
|
||
| 每个已挂载的通知 provider 拥有内部 EventTarget,通过 React context 与其后代 App 共享。通知点击使用该 target,替代 window 全局广播,因此只有所属 Web Shell 会导航。已有 window qwen:open-session 事件继续服务 Markdown 链接。通知目标与 App 当前锁定工作区冲突时会被忽略,包括宿主修改锁定工作区后点击旧通知。所属实例卸载后,旧目标不再有活动导航监听。 | ||
|
|
||
| ## 验证 | ||
|
|
||
| 测试覆盖公开入口 provider 层级、传入与省略配置、默认及部分/空白配置、CDN URL 传递、更新配置不重新挂载、权限与关闭偏好保持、实例间通知导航隔离、锁定工作区拒绝及现有独立页面导航路径。组件库构建必须在发布产物中包含默认图片。浏览器验证使用导入公开入口的宿主 fixture,传入自定义品牌并执行真实 daemon 回合;API 采集验证名称、提问/回复及图片 URL,不代表 OS 图标位置或远端 CDN 可用性已验证。 |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,33 @@ | ||
| # Browser notification details and session navigation | ||
|
|
||
| [English](web-shell-browser-notification-details.md) | [简体中文](web-shell-browser-notification-details.zh-CN.md) | ||
|
|
||
| Following the base implementation in #11398, this enhancement lets users identify the session behind a notification, preview this turn's reply, and click to return to that session. Permissions, background triggers, and deduplication remain those of the base implementation; the built-in entry now defaults the preference on as described in the branding design. The public Web Shell branding options are described in the [branding design](web-shell-browser-notification-branding.md); no daemon route or wire field is added. | ||
|
|
||
| ## Content and icon | ||
|
|
||
| The default title is QwenCode · session title; hosts can override the application name and icon through the [branding options](web-shell-browser-notification-branding.md). If a new session has no title yet or its title has been cleared, use the first visible line of this turn's request after removing common Markdown formatting; if neither exists, display QwenCode. The body retains the turn status and includes a plain-text excerpt from the last main-assistant reply for a completed turn, without another model call to summarize it. Extraction matches the terminal promptId exactly and excludes tools, thoughts, background agents, and internal insight messages. Missing text does not fall back to an older turn, and failures do not expose partial replies or error details. | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [Suggestion] R3-7: Both language versions of this new design scope the reply excerpt to a completed turn, but the shipped code attaches it to every non- A long task hits the output token cap; the daemon emits One correction in your favour, because the finder's framing overstated it: the base design's outcome table constrains the status wording, and the shipped status honours it — the banner says "Return to check the result", not "completed". So this is not a turn being presented as a finished result. What survives is the concrete, checkable divergence: the sentence this PR adds scopes the excerpt to a completed turn, and the code ships it for a definitionally partial one. The path is also unpinned, so whichever way you resolve it a later change can flip it silently: every body assertion in Witness: Resolve in one direction and keep both languages in step. Either narrow the code — Fix constraint: Fix witness: add a case to 中文说明这份新设计的两个语言版本都把回复摘录限定为已完成的回合,但实际代码对除 一个长任务触到输出 token 上限;daemon 发出带 有一处需要替你们纠正,因为原始发现的表述夸大了:基线设计的结局表约束的是状态文案,而实际状态文案是遵守的——横幅说的是“请返回查看结果”,不是“已完成”。所以这不是把一个回合呈现为已完成的结果。仍然成立的是那个具体、可核对的分歧:本 PR 新增的这句话把摘录限定为已完成回合,而代码对一个按定义被截断的回合也发送摘录。 这条路径也没有测试锁定,所以无论你们选哪个方向,之后的改动都可能静默翻转它: 请择一方向解决,并保持两种语言同步。要么收窄代码—— 修复约束: 修复验收:在 — qwen3.8-max via Qwen Code /review (v0.23.2) |
||
|
|
||
| Extraction runs after the live/replay transcript projection. Titles use the owning session's state and owner-matched connection metadata. The main chat suppresses the submitting user's SSE echo, and the local optimistic user block may lack a promptId. The observer therefore retains a title fallback through the existing admission promptId/label; live pending-start events can also supply it. Labels remain only in the pending-turn memory Map and are removed on consumption, removal, or release of the last observer. | ||
|
|
||
| The body also includes this turn's user prompt, with localized Prompt and Reply labels after the status. Extract the first non-empty main-user block matching the terminal promptId, independently of the session title. When the optimistic block has no promptId, retain up to 4096 UTF-16 code units of the existing admission label (or live pending-event text) in the pending Map as a fallback; the title fallback takes its first visible line. Transport-added trailing attachment-token lines are omitted from notification prompts. The pending map retains at most 1024 entries and clears injected message IDs when mid-turn injection is observed. Do not borrow another turn's prompt or use the session title as the prompt. Failures may include the prompt but never partial replies or error details. Missing prompt or reply text omits that line. | ||
|
|
||
| The session-title component retains at most 60 Unicode code points (the application prefix is separate), prompt excerpts at most 80, and reply excerpts at most 120, ending in an ellipsis when truncated. Excerpt cleanup reads at most the first 4096 UTF-16 code units, removes common Markdown formatting and simple inline link destinations, strips control and bidi-formatting characters, and collapses whitespace. Angle-bracket text, including HTML and JSX, is preserved literally because a plain-text notification must not guess whether it is code or markup. Fenced code keeps its content. Truncation avoids splitting Unicode code points; grapheme-cluster truncation is out of scope. No extra configuration or model call is introduced. Browser/OS layout may truncate the combined body further. | ||
|
|
||
| Notifications include a package-local copy of the project's existing 128×128 PNG mark from `packages/desktop-shell/src-tauri/icons/128x128.png`, with Vite resolving its URL. The published library inlines the PNG as a data URL so it needs no separately published asset. Business text does not append IP addresses, ports, or navigation URLs. Chrome's site attribution and its own macOS logo are controlled by the browser and OS and cannot be replaced by the business icon. Content may appear in the notification center or on the lock screen; the setting description explains that titles, prompts, and reply excerpts are shown. Shared storage continues to record only deduplication hashes, never bodies, titles, or navigation targets. | ||
|
|
||
| ## Clicking to open a session | ||
|
|
||
| The internal owner binding captures sessionId and product context: workspace uses the resolved owner.workspaceCwd, while standalone/live omit internal working directories. The target remains in the notification's in-memory closure. Clicking attempts to focus the original window, passes the explicit target to the existing qwen:open-session entry point, and closes the notification. Navigation is still attempted if the browser refuses focus. | ||
|
|
||
| App reuses the existing session-loading flow for settings, split view, draft context, asynchronous navigation, and failure reporting. Existing session links without explicit context keep their behavior. Malformed or conflicting explicit contexts are rejected instead of falling back to the current or primary workspace. If the target is already the healthy current session, its context matches, and there is no pending draft intent, only reveal chat, restore composer focus when leaving split view, and close panels to avoid disrupting the next turn with a duplicate load. Split-view notifications open the main chat, and the existing callback updates the address bar after a successful load. | ||
|
|
||
| The original page and main App navigation listener must still be alive. Expired sessions, removed workspaces, and load failures use existing error feedback without creating a session or switching to another workspace. Closed-page delivery, Channel, Service Worker, and Web Push are outside this enhancement. | ||
|
|
||
| ## Validation scope | ||
|
|
||
| Content is projected lazily after terminal identity, replay, and deduplication checks. Insight segmentation reuses the transcript renderer: visible text is retained while internal payloads are omitted; payload-only final blocks never borrow an earlier reply. | ||
|
|
||
| Unit tests cover live/replay content projection, admission title fallback, exact-turn and main-assistant filtering, Unicode truncation, failure and empty-content fallbacks, icon parameters, captured targets, cross-workspace and standalone/live navigation, exiting settings and split view, avoiding duplicate current-session loads, legacy links, and focus failure. | ||
|
|
||
| Page verification uses an isolated runtime and Chrome: capture the notification produced after a real submission settles, switch to another session and open settings, then invoke the captured notification's click callback. Check that the original session and URL return, settings exits, and the notification closes. A Notification stub and simulated loss of focus demonstrate browser parameters and navigation, not actual OS banner or icon display. The icon is separately checked for production HTTP availability, PNG signature, and dimensions. Detailed test plans and results are kept locally under .qwen/e2e-tests/. | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[Suggestion] R3-6: This title-format clause contradicts the companion design added by this same PR and the shipped code, for the untitled-session case. Both language versions carry the identical error, so it is not a translation gap — the two twins disagree with
details.mdand with the code, not with each other.This line says the application name appears alone when there is no session title. In reality an untitled session that has a prompt shows
appName · <first visible line of the prompt>:turn-notification-context.ts:232-233setscontent.sessionTitle = notificationTextLines(content.promptText)[0]when!sessionTitle?.trim()(again at:391-393), andbrowser-turn-notifications.tsx:257composestitle ? appName · title : appName. App-name-alone occurs only when neither a title nor a prompt exists — which is what the companiondetails.md:9says in both languages ("if neither exists, display QwenCode").The audience for this document is a host integrator, and the cost is that they read the branding design, conclude an untitled session's OS banner shows only their application name in the title, and repeat that in their own integration or privacy-facing copy — while conversation content is in fact in the title. Two designs added in one change assigning different titles to the same input is the concrete defect; it does not need a documentation rule to stand.
Witness:
Align this sentence with
details.mdin both languages, e.g. EN: "The title remains application name · session title; when the session has no title, the session-title part falls back to the first visible line of this turn's prompt, and the application name is shown alone only when neither exists." zh: 「标题保持“应用名称 · 会话标题”;会话没有标题时,会话标题部分回退到本轮提问的首个可见行,两者均缺失时才只显示应用名称。」Fix constraint: the rewritten sentence must describe the implemented fallback rather than restate the app-name-alone rule —
content.sessionTitle = notificationTextLines(content.promptText)[0];atpackages/web-shell/client/daemon/session/turn-notification-context.ts:233, consumed bypackages/web-shell/client/browser-turn-notifications.tsx:257— anddocs/design/README.mdrequires both language versions to be updated in the same change, so the.mdand.zh-CN.mdedits must land together.中文说明
这条标题格式条款与本次同一个 PR 新增的姊妹设计文档以及实际代码相矛盾,矛盾点在“会话没有标题”这一情形。两个语言版本带有完全相同的错误,所以这不是翻译差异——两个语言版本是与
details.md和代码不一致,而不是彼此不一致。这一行说:没有会话标题时只显示应用名称。实际上,一个没有标题但有提问的会话显示的是
appName · <提问的首个可见行>:turn-notification-context.ts:232-233在!sessionTitle?.trim()时设置content.sessionTitle = notificationTextLines(content.promptText)[0](:391-393重复一次),browser-turn-notifications.tsx:257组装title ? appName · title : appName。只有在标题和提问都不存在时才只显示应用名称——这正是姊妹文档details.md:9在两种语言中所说的(“两者均缺失时显示 QwenCode”)。本文档的读者是宿主接入方,代价是:他们读了品牌设计文档后认为未命名会话的系统横幅标题只显示自己的应用名,并把这一点写进自己的接入说明或面向隐私的文案中——而实际上标题里含有对话内容。同一次改动中新增的两份设计对同一输入给出不同标题,这本身就是具体缺陷,不需要依赖任何文档规范就能成立。
建议把这句话在两种语言中都与
details.md对齐,例如中文:「标题保持“应用名称 · 会话标题”;会话没有标题时,会话标题部分回退到本轮提问的首个可见行,两者均缺失时才只显示应用名称。」修复约束:改写后的句子必须描述已实现的回退行为,而不是重述“只显示应用名称”的规则——见
packages/web-shell/client/daemon/session/turn-notification-context.ts:233的content.sessionTitle = notificationTextLines(content.promptText)[0];,由packages/web-shell/client/browser-turn-notifications.tsx:257消费;并且docs/design/README.md要求两个语言版本在同一次改动中一起更新,所以.md与.zh-CN.md的修改必须同时落地。— qwen3.8-max via Qwen Code /review (v0.23.2)