diff --git a/packages/experimental/client-ui-plugin-recommendations/README.i18n.yaml b/packages/experimental/client-ui-plugin-recommendations/README.i18n.yaml index b4782e4316..85efd163d6 100644 --- a/packages/experimental/client-ui-plugin-recommendations/README.i18n.yaml +++ b/packages/experimental/client-ui-plugin-recommendations/README.i18n.yaml @@ -18,8 +18,8 @@ en: 62e446e9cb825f36 zh: 620506f3986eb9a9 /deepseek-ai-dsh-experimental-client-ui-plugin-recommendations/understand-the-implementation: - en: 5cc1ecf907eabad8 - zh: 3ad6d1afc6944a6b + en: d53f149533596c28 + zh: 77e539467885f600 /deepseek-ai-dsh-experimental-client-ui-plugin-recommendations/further-exploration: en: 857c404e794d16f9 zh: 9e907212efe09dd9 @@ -30,8 +30,8 @@ en: 4faf0dd1beff482e zh: 77ea65782433f221 /deepseek-ai-dsh-experimental-client-ui-plugin-recommendations/known-limitations-and-deferred-work: - en: 49c565c266959430 - zh: 0b380afd42533f2d + en: a1dd9c7a4fbd5971 + zh: 8909053269549211 /deepseek-ai-dsh-experimental-client-ui-plugin-recommendations/known-limitations-and-deferred-work/dev-note: en: e67f633dc6fe7773 zh: 8ec3e73528ddb1a6 diff --git a/packages/experimental/client-ui-plugin-recommendations/README.md b/packages/experimental/client-ui-plugin-recommendations/README.md index 12a54bb8e9..a3e6a9e0b7 100644 --- a/packages/experimental/client-ui-plugin-recommendations/README.md +++ b/packages/experimental/client-ui-plugin-recommendations/README.md @@ -35,7 +35,7 @@ Enable the [Plugin recommendations bundle](../plugin-recommendations/README.md).
Implementation internals — click to expand -The Turn tail reads the proposals of the `pluginRecommendations` Session projection and shows the one admitted in its Turn once that Turn has ended, for any end reason; the projection admits at most one proposal per Turn, and the chat hides the fence itself. A proposal the projection marks never offered renders nothing. The card asks the Host for the plugin's current catalog text and selection; a name outside the eligible catalog shows no actions. Session projection decisions drive the card after reload. The optional Remote and all UI registrations are removed together on unload. +The Turn tail reads the proposals of the `pluginRecommendations` Session projection and shows the one admitted in its Turn once that Turn has ended, for any end reason; the projection admits at most one proposal per Turn, and the chat hides the fence itself. A proposal the projection marks never offered renders nothing. The card asks the Host for the plugin's current catalog text and selection; a name outside the eligible catalog shows no actions. The browser contribution keeps returned catalog text by plugin name for its lifetime, so a revisited card renders it at once while its own lookup refreshes the selection. Until a plugin's first lookup returns, placeholder lines and the disabled actions hold the title, description and footer in place. Session projection decisions drive the card after reload. The optional Remote and all UI registrations are removed together on unload. Cards use the application font, the deliverables card border and background, a distinct title and compact body scale, and a separate action footer. The built entry supplies generated Remote descriptors to the source-tested mount lifecycle. @@ -67,6 +67,7 @@ The card adds no model context. Its Enable action can change subsequent tool dec - Only the first proposal in a Turn appears. +- Before a plugin's first lookup returns, the card reserves one description line; a longer description still extends the card when it arrives. - Historical decisions do not track later changes made from the Plugins page. diff --git a/packages/experimental/client-ui-plugin-recommendations/README.zh.md b/packages/experimental/client-ui-plugin-recommendations/README.zh.md index 3c6fc8c636..47873c74b8 100644 --- a/packages/experimental/client-ui-plugin-recommendations/README.zh.md +++ b/packages/experimental/client-ui-plugin-recommendations/README.zh.md @@ -35,7 +35,7 @@ kind: "package-reference"
实现细节 — 点击展开 -回合末尾插槽读取 `pluginRecommendations` Session 投影中的提案,在回合结束后(无论以何种原因结束)显示该回合接纳的那一条;投影每个回合最多接纳一条提案,聊天界面隐藏围栏本身。投影标为未展示的提案不渲染。卡片向 Host 读取插件当前的目录文本和启用状态;不在可推荐目录里的名字不显示操作按钮。Session 投影恢复刷新后的选择状态。卸载时同时移除可选 Remote 和所有界面注册。 +回合末尾插槽读取 `pluginRecommendations` Session 投影中的提案,在回合结束后(无论以何种原因结束)显示该回合接纳的那一条;投影每个回合最多接纳一条提案,聊天界面隐藏围栏本身。投影标为未展示的提案不渲染。卡片向 Host 读取插件当前的目录文本和启用状态;不在可推荐目录里的名字不显示操作按钮。浏览器端在插件生命周期内按插件名记住读到的目录文本,再次进入会话时卡片立即显示,同时卡片自己的查询会刷新启用状态。某个插件的第一次查询返回前,卡片用占位行和禁用的按钮占住标题、描述和操作区的位置。Session 投影恢复刷新后的选择状态。卸载时同时移除可选 Remote 和所有界面注册。 卡片沿用应用字体和交付物卡片的边框与背景,通过标题、正文和独立操作区区分信息层级。构建入口将生成的 Remote 描述传给可直接从源码测试的挂载流程。 @@ -67,6 +67,7 @@ kind: "package-reference" - 每个回合只显示第一条推荐。 +- 插件第一次查询返回前,卡片只预留一行描述;更长的描述返回后卡片仍会变高。 - 历史选择不会跟踪之后在插件页面进行的修改。 diff --git a/packages/experimental/client-ui-plugin-recommendations/src/client/Card.module.css b/packages/experimental/client-ui-plugin-recommendations/src/client/Card.module.css index f280fd4e55..67917d497c 100644 --- a/packages/experimental/client-ui-plugin-recommendations/src/client/Card.module.css +++ b/packages/experimental/client-ui-plugin-recommendations/src/client/Card.module.css @@ -9,6 +9,7 @@ .title { display: block; font-size: 16px; font-weight: 500; line-height: 1.5; overflow-wrap: anywhere; } .reason { margin: 8px 0; font-size: 13px; line-height: 1.7; white-space: pre-wrap; overflow-wrap: anywhere; } .description { margin: 0; font-size: 13px; line-height: 1.7; color: var(--dsw-alias-label-secondary); overflow-wrap: anywhere; } +.placeholder { display: inline-block; width: 50%; height: 0.8em; vertical-align: middle; border-radius: var(--dsw-radius-xs); background: var(--dsw-alias-bg-skeleton); } .footer { display: flex; align-items: center; justify-content: space-between; gap: 12px; margin-top: 16px; padding-top: 16px; border-top: 0.5px solid var(--dsw-alias-border-l1); flex-wrap: wrap; } .scope { font-size: 12px; line-height: 1.5; color: var(--dsw-alias-label-secondary); } .actions { display: flex; align-items: center; gap: 8px; margin-left: auto; } diff --git a/packages/experimental/client-ui-plugin-recommendations/src/client/Card.tsx b/packages/experimental/client-ui-plugin-recommendations/src/client/Card.tsx index 4d8a16d34a..7447d4a64b 100644 --- a/packages/experimental/client-ui-plugin-recommendations/src/client/Card.tsx +++ b/packages/experimental/client-ui-plugin-recommendations/src/client/Card.tsx @@ -11,13 +11,18 @@ import type {} from '@deepseek-ai/dsh-client-ui-chat/client' import type { RecommendationKey } from './locales.ts' import css from './Card.module.css' -/** Session-bound callbacks and language, without a service in the component. */ +/** Catalog text the Host returned for an available plugin. */ +export type CatalogDetails = Extract + +/** Session-bound callbacks, language and the plugin-lifetime catalog text, without a service in the component. */ export interface CardInjected { act: (callId: ToolCallId, action: 'enable' | 'dismiss') => Promise describe: (callId: ToolCallId) => Promise + /** Keep a plugin's catalog text so later cards of the same plugin render it at once. */ + remember: (name: string, details: CatalogDetails) => void openDetails: (packageName: string) => void resolveText: (text: LocalizedText) => string - hooks: { locale: HostObservable } + hooks: { locale: HostObservable; catalog: HostObservable>> } } type CardProps = { recommendation: PluginRecommendation } & Pick, 'useProjection'> @@ -49,28 +54,39 @@ export function RecommendationTail({ turn, ...props }: Pick value) const view = useProjection('pluginRecommendations') const state = view?.decisions[recommendation.callId] - // Undefined while loading; null when the Host could not be reached. - const [details, setDetails] = useState() + const cached = useCatalog(catalog => Object.hasOwn(catalog, recommendation.name) ? catalog[recommendation.name] : undefined) + // This card's latest lookup: undefined while loading, null when the Host could not be reached. + const [lookup, setLookup] = useState() const [pending, setPending] = useState(false) const [error, setError] = useState() useEffect(() => { let current = true describe(recommendation.callId).then( - (value) => { if (current) setDetails(value ?? null) }, - () => { if (current) setDetails(null) }, + (value) => { + if (!current) return + if (value?.status === 'available') remember(recommendation.name, value) + setLookup(value ?? null) + }, + () => { if (current) setLookup(null) }, ) return () => { current = false } - }, [describe, recommendation.callId, state]) + }, [describe, remember, recommendation.callId, recommendation.name, state]) + // Remembered text shows at once; this card's own refusal or lost reply still decides what it offers. + const details = lookup === null || lookup?.status === 'refused' ? lookup : cached ?? lookup + const loading = details === undefined const available = details?.status === 'available' ? details : undefined const settled = state ?? (available?.enabled ? 'already-enabled' : undefined) - const open = available !== undefined && !view?.invalid && (settled === undefined || settled === 'failed' || settled === 'cancelled') + const open = (available !== undefined || loading) && !view?.invalid && (settled === undefined || settled === 'failed' || settled === 'cancelled') const run = async (action: 'enable' | 'dismiss') => { setPending(true) setError(undefined) @@ -85,14 +101,16 @@ export function RecommendationCard({ recommendation, useProjection, useLocale, a } finally { setPending(false) } } const title = available === undefined ? recommendation.name : resolveText(available.title) - return
+ return
{t('heading')}{t('official')}
{settled !== undefined &&

{t(settled)}

} {view?.invalid ?

{t('invalid-history')}

@@ -102,8 +120,8 @@ export function RecommendationCard({ recommendation, useProjection, useLocale, a {open &&