mirror of
https://github.com/QwenLM/qwen-code.git
synced 2026-11-07 06:19:43 -10:00
feat(web-shell): open context details in the side panel from the ring (#14086)
Clicking the composer context ring posted a Context Usage card into the transcript, and that card's own detail action posted a second one, so checking usage a few times left several near-identical stale snapshots interleaved with the conversation and pulled the view back to the tail. The right panel already carries a single-instance context usage tab keyed per session, with the full breakdown, refresh and compression. Bind the ring click and the transcript card's detail action to it instead. The snapshot survives only for a composer whose host offers no details surface, which today is just the embedded side-task pane; typing /context still prints into the transcript. Closes #14051 Co-authored-by: Qwen-Coder <[email protected]>
This commit is contained in:
1 parent
05dd6e979e
commit
b0cee09604
11 files changed
+222
-60
No files matched your search
@@ -0,0 +1,40 @@
|
||||
# Web Shell context ring opens the details panel
|
||||
|
||||
[English](2026-10-13-web-shell-context-ring-details.md) | [简体中文](2026-10-13-web-shell-context-ring-details.zh-CN.md)
|
||||
|
||||
## Problem and scope
|
||||
|
||||
Clicking the composer context ring posted a `Context Usage` snapshot card into the transcript, and that card's own detail action posted a second one. Checking usage a few times during a long session left several near-identical cards interleaved with the conversation, each frozen at a different moment, and each click pulled the transcript back to the tail even while the user was scrolled up. Issue #14051.
|
||||
|
||||
Hover already shows the summary, so the click was repeating it somewhere else instead of going one level deeper.
|
||||
|
||||
## Design
|
||||
|
||||
Reuse the one details surface that already exists: the right panel's context usage tab. Its id is `context-usage:${sessionId}` and its opener replaces a matching tab instead of appending, so repeated clicks cannot stack. It already renders the per-category breakdown with expandable items, refresh, compression, and load and error states.
|
||||
|
||||
The change is therefore a rebinding, not a new surface:
|
||||
|
||||
- The ring click opens that panel instead of posting a card.
|
||||
- A transcript context card's detail action opens the same panel instead of posting a second card.
|
||||
- Hover is unchanged, and the popover keeps its own detail action, because keyboard and touch have no hover.
|
||||
- Typing `/context` or `/context detail` still prints into the transcript: the user asked for it there explicitly.
|
||||
|
||||
An anchored popover or dialog was rejected. A full breakdown in a toolbar-anchored popover needs collision, scroll, and focus-trap work, and a dialog has to go through the portal root; either way the app would carry two details surfaces for the same data unless the panel were removed as well.
|
||||
|
||||
### Fallback
|
||||
|
||||
A composer whose host offers no details surface keeps the snapshot. Today that is only the embedded side-task pane, which already lives inside the right panel and has nowhere to open a tab. Its ring still posts `/context` into its own conversation, and only that case shows the hover hint saying so, because the old copy would otherwise be false. The main composer always has a panel, so its snapshot wiring was removed rather than left unreachable.
|
||||
|
||||
## Known limitation
|
||||
|
||||
Reopening an already-open panel does not refetch. The panel is a persistent surface with its own refresh control, like a file preview: clicking its opener again focuses it rather than reloading it. A follow-up could refresh on reopen, or toggle the panel closed.
|
||||
|
||||
## Files and validation
|
||||
|
||||
`client/components/ChatEditor.tsx`, `client/App.tsx`, their unit tests, and the two browser specs that asserted the old card stacking. The package README and the context usage callback design doc change in both languages, because the host callback now also receives ring clicks and card detail clicks.
|
||||
|
||||
Verify: hover summary unchanged; a click opens one panel tab and a second click keeps it at one; no click adds a transcript card; `/context` and `/context detail` still print cards; a card's detail action opens the panel; the embedded side-task ring still posts a snapshot and still shows the hint; a split pane opens its own session's tab. Run build, typecheck, lint, the web-shell unit tests, and the two context browser specs.
|
||||
|
||||
## Acceptance and risks
|
||||
|
||||
Checking context usage no longer writes to the conversation, and every entry point reaches one details surface. Risks: a host that relied on a ring click producing a transcript card now gets the panel or its own callback; the embedded pane keeps the old behavior on purpose. No daemon routes or data contracts change.
|
||||
@@ -0,0 +1,40 @@
|
||||
# Web Shell 上下文圆环打开详情面板
|
||||
|
||||
[English](2026-10-13-web-shell-context-ring-details.md) | [简体中文](2026-10-13-web-shell-context-ring-details.zh-CN.md)
|
||||
|
||||
## 问题与范围
|
||||
|
||||
点击 composer 的上下文圆环会往对话记录里插入一张 `Context Usage` 快照卡片,而这张卡片自己的详情入口又会插入第二张。长会话里查看几次用量,对话中就会留下好几张几乎相同的卡片,各自冻结在不同时刻,而且每次点击都会把对话拉回末尾,即使用户当时正在往上翻。见 issue #14051。
|
||||
|
||||
悬停已经展示了摘要,所以点击只是把同样的内容换个地方再说一遍,而不是进入下一层。
|
||||
|
||||
## 设计
|
||||
|
||||
复用已经存在的那一个详情界面:右侧面板的上下文用量标签页。它的 id 是 `context-usage:${sessionId}`,打开逻辑遇到同 id 的标签页是替换而不是追加,所以重复点击在结构上就不可能叠出第二份。它已经渲染了逐分类的明细、可展开的条目、刷新、压缩,以及加载和错误状态。
|
||||
|
||||
因此这次改动是改绑入口,而不是新做一套界面:
|
||||
|
||||
- 圆环点击打开该面板,不再插入卡片。
|
||||
- 对话内上下文卡片的详情入口打开同一个面板,不再插入第二张卡片。
|
||||
- 悬停行为不变;弹层保留自己的详情入口,因为键盘和触屏没有悬停。
|
||||
- 手动输入 `/context` 或 `/context detail` 仍然输出到对话记录:那是用户明确要求放在那里的。
|
||||
|
||||
锚定弹层和对话框方案被否决。把完整明细放进锚定在工具栏上的弹层,需要处理碰撞、滚动和焦点陷阱;做成对话框则必须走 portal root。两种方式都会让同一份数据存在两个详情界面,除非同时把面板删掉。
|
||||
|
||||
### 回落
|
||||
|
||||
宿主没有提供详情入口的 composer 保留快照行为。目前只有内嵌的 side-task 面板属于这种情况——它本身就在右侧面板里,没有地方再开一个标签页。它的圆环仍会在自己的对话中生成 `/context`,并且只有这种情况才显示说明这一点的悬停提示,否则旧文案就是不成立的。主 composer 恒定有面板,所以它的快照接线被直接删除,而不是留成一条永远走不到的分支。
|
||||
|
||||
## 已知限制
|
||||
|
||||
面板已经打开时再次打开不会重新读取。面板是一个带自己刷新控件的持久界面,和文件预览一样:再点一次它的入口是聚焦,而不是重新加载。后续可以改成重新打开时刷新,或者切成开合切换。
|
||||
|
||||
## 文件与验证
|
||||
|
||||
`client/components/ChatEditor.tsx`、`client/App.tsx`、它们的单元测试,以及两个断言了旧卡片堆叠行为的浏览器 spec。包 README 和上下文用量回调设计文档的中英版本都要改,因为宿主回调现在也会收到圆环点击和卡片详情点击。
|
||||
|
||||
验证:悬停摘要不变;点击打开一个面板标签页,再点仍是一个;点击不会往对话记录里加卡片;`/context` 和 `/context detail` 仍会输出卡片;卡片的详情入口打开面板;内嵌 side-task 的圆环仍生成快照并仍显示提示;分屏打开自己会话的标签页。执行构建、类型检查、lint、web-shell 单元测试和两个上下文浏览器 spec。
|
||||
|
||||
## 验收与风险
|
||||
|
||||
查看上下文用量不再写入对话,所有入口都收敛到同一个详情界面。风险:依赖圆环点击产生对话卡片的宿主,现在会拿到面板或自己的回调;内嵌面板是有意保留旧行为的。不改变 daemon 路由或数据协议。
|
||||
@@ -16,10 +16,12 @@ returns before creating or selecting a built-in panel tab. Without the callback,
|
||||
existing behavior is preserved. Both `WebShell` and `WebShellWithProviders`
|
||||
expose the prop through their existing public types.
|
||||
|
||||
The callback applies to composer details and header detail entries, including
|
||||
split panes. It does not intercept the ring’s `/context` snapshot action,
|
||||
compression, or restoration of already persisted panel tabs. It is an override,
|
||||
not a notification; no return-value protocol or extra UI is needed.
|
||||
The callback applies to the composer ring click, composer details, transcript
|
||||
context cards, and header detail entries, including split panes. It does not
|
||||
intercept compression, restoration of already persisted panel tabs, or the
|
||||
`/context` snapshot fallback that a composer with no details surface still
|
||||
uses. It is an override, not a notification; no return-value protocol or extra
|
||||
UI is needed.
|
||||
|
||||
## Files and validation
|
||||
|
||||
|
||||
@@ -15,9 +15,10 @@ Composer 上下文用量的 hover 操作会打开 Web Shell 右侧面板。嵌
|
||||
返回。不提供时保留现有行为。`WebShell` 和 `WebShellWithProviders` 通过
|
||||
现有公开类型暴露此参数。
|
||||
|
||||
回调适用于 composer 详情和页头详情入口,包括分屏。它不拦截圆环的
|
||||
`/context` 快照操作、压缩操作或已持久化面板标签的恢复。它是打开行为的
|
||||
替代,而非通知;无需返回值协议或额外 UI。
|
||||
回调适用于 composer 圆环点击、composer 详情、对话内上下文卡片和页头详情
|
||||
入口,包括分屏。它不拦截压缩操作、已持久化面板标签的恢复,以及没有详情入口
|
||||
的 composer 仍会使用的 `/context` 快照回落。它是打开行为的替代,而非通知;
|
||||
无需返回值协议或额外 UI。
|
||||
|
||||
## 文件与验证
|
||||
|
||||
|
||||
@@ -555,10 +555,12 @@ daemon 参数的完整含义和配置方式见
|
||||
<WebShell onContextUsageOpen={(sessionId) => openContextDetails(sessionId)} />
|
||||
```
|
||||
|
||||
此参数也适用于 `WebShellWithProviders`。提供回调后,composer hover 弹层的
|
||||
「查看明细」及页头上下文详情入口会将来源会话 ID 交给宿主,不再打开内置右侧
|
||||
面板或自动读取详情;分屏传入对应分屏的会话 ID。不提供回调则保留默认行为。
|
||||
直接点击上下文圆环生成 `/context` 快照、压缩操作和已保存面板的恢复不受影响。
|
||||
此参数也适用于 `WebShellWithProviders`。提供回调后,上下文圆环的点击、
|
||||
composer hover 弹层的「查看明细」、对话内上下文卡片的「查看明细」以及页头
|
||||
上下文详情入口都会将来源会话 ID 交给宿主,不再打开内置右侧面板或自动读取
|
||||
详情;分屏传入对应分屏的会话 ID。不提供回调则保留默认行为。压缩操作和已保存
|
||||
面板的恢复不受影响。内嵌的 side-task 面板本身就在右侧面板里,没有可用的详情
|
||||
入口,它的圆环点击仍会在该面板的对话中生成 `/context` 快照。
|
||||
|
||||
宿主可以监听命令,也可以返回 `true` 接管对应操作:
|
||||
|
||||
|
||||
@@ -16707,7 +16707,7 @@ describe('App session callbacks', () => {
|
||||
expect(container.textContent).not.toContain('qwen-plus::hybrid');
|
||||
});
|
||||
|
||||
it('wires the composer context ring from the connection and opens /context on click', async () => {
|
||||
it('wires the composer context ring from the connection and opens the details panel on click', async () => {
|
||||
const usageConnection = mockConnection as typeof mockConnection & {
|
||||
tokenCount?: number;
|
||||
contextWindow?: number;
|
||||
@@ -16715,6 +16715,10 @@ describe('App session callbacks', () => {
|
||||
usageConnection.tokenCount = 338;
|
||||
usageConnection.contextWindow = 1000;
|
||||
|
||||
mockSessionActions.getContextUsage.mockResolvedValue({
|
||||
...paneContextFixture,
|
||||
sessionId: 'session-1',
|
||||
});
|
||||
renderApp();
|
||||
await flush();
|
||||
|
||||
@@ -16723,15 +16727,45 @@ describe('App session callbacks', () => {
|
||||
);
|
||||
expect(testState.latestChatEditorProps?.tokenCount).toBe(338);
|
||||
expect(testState.latestChatEditorProps?.contextWindow).toBe(1000);
|
||||
// The main composer always has a details panel, so its ring never falls
|
||||
// back to posting a snapshot card into the transcript.
|
||||
expect(testState.latestChatEditorProps?.onShowContextUsage).toBeUndefined();
|
||||
|
||||
await act(async () => {
|
||||
testState.latestChatEditorProps?.onShowContextUsage?.();
|
||||
testState.latestChatEditorProps?.onOpenContextUsage?.();
|
||||
});
|
||||
await flush();
|
||||
|
||||
expect(mockSessionActions.getContextUsage).toHaveBeenCalledWith({
|
||||
detail: false,
|
||||
detail: true,
|
||||
silent: true,
|
||||
});
|
||||
expect(mockStore.appendLocalUserMessage).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('opens the details panel from a transcript context card', async () => {
|
||||
mockSessionActions.getContextUsage.mockResolvedValue({
|
||||
...paneContextFixture,
|
||||
sessionId: 'session-1',
|
||||
});
|
||||
renderApp();
|
||||
await flush();
|
||||
|
||||
const onShowContextDetail = (
|
||||
testState.latestMessageListProps as unknown as {
|
||||
onShowContextDetail?: () => void;
|
||||
} | null
|
||||
)?.onShowContextDetail;
|
||||
expect(onShowContextDetail).toBeTypeOf('function');
|
||||
|
||||
await act(async () => onShowContextDetail!());
|
||||
await flush();
|
||||
|
||||
expect(mockSessionActions.getContextUsage).toHaveBeenCalledWith({
|
||||
detail: true,
|
||||
silent: true,
|
||||
});
|
||||
expect(mockStore.appendLocalUserMessage).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it.each(['main', 'split'] as const)(
|
||||
@@ -16766,7 +16800,7 @@ describe('App session callbacks', () => {
|
||||
},
|
||||
);
|
||||
|
||||
it('updates and removes the host context usage callback without changing snapshots', async () => {
|
||||
it('updates and removes the host context usage callback', async () => {
|
||||
const onContextUsageOpen = vi.fn();
|
||||
const replacement = vi.fn();
|
||||
const { rerender } = renderApp({ onContextUsageOpen });
|
||||
@@ -16778,12 +16812,7 @@ describe('App session callbacks', () => {
|
||||
);
|
||||
expect(replacement).toHaveBeenCalledExactlyOnceWith('session-1');
|
||||
expect(onContextUsageOpen).not.toHaveBeenCalled();
|
||||
await act(async () =>
|
||||
testState.latestChatEditorProps!.onShowContextUsage!(),
|
||||
);
|
||||
expect(mockSessionActions.getContextUsage).toHaveBeenCalledWith({
|
||||
detail: false,
|
||||
});
|
||||
expect(mockSessionActions.getContextUsage).not.toHaveBeenCalled();
|
||||
expect(replacement).toHaveBeenCalledTimes(1);
|
||||
mockSessionActions.getContextUsage.mockClear();
|
||||
rerender({});
|
||||
@@ -16895,7 +16924,7 @@ describe('App session callbacks', () => {
|
||||
await flush();
|
||||
expect(testState.latestChatEditorProps?.tokenCount).toBe(23_000);
|
||||
expect(testState.latestChatEditorProps?.contextWindow).toBe(131_072);
|
||||
expect(testState.latestChatEditorProps?.onShowContextUsage).toBeTypeOf(
|
||||
expect(testState.latestChatEditorProps?.onOpenContextUsage).toBeTypeOf(
|
||||
'function',
|
||||
);
|
||||
|
||||
@@ -16906,7 +16935,7 @@ describe('App session callbacks', () => {
|
||||
expect(testState.latestChatEditorProps?.tokenCount).toBe(0);
|
||||
expect(testState.latestChatEditorProps?.contextWindow).toBe(0);
|
||||
expect(
|
||||
testState.latestChatEditorProps?.onShowContextUsage,
|
||||
testState.latestChatEditorProps?.onOpenContextUsage,
|
||||
).toBeUndefined();
|
||||
|
||||
mockConnection.status = 'connected';
|
||||
@@ -16914,7 +16943,7 @@ describe('App session callbacks', () => {
|
||||
await flush();
|
||||
expect(testState.latestChatEditorProps?.tokenCount).toBe(23_000);
|
||||
expect(testState.latestChatEditorProps?.contextWindow).toBe(131_072);
|
||||
expect(testState.latestChatEditorProps?.onShowContextUsage).toBeTypeOf(
|
||||
expect(testState.latestChatEditorProps?.onOpenContextUsage).toBeTypeOf(
|
||||
'function',
|
||||
);
|
||||
} finally {
|
||||
|
||||
@@ -13929,9 +13929,9 @@ export function App({
|
||||
// so the panel is revealed even when the click comes while scrolled up.
|
||||
const showContextUsage = useCallback(
|
||||
(commandText: string, detail: boolean) => {
|
||||
// Read-only: every entry point (keyboard, status-bar button, in-chat
|
||||
// "context detail" click) runs immediately, even mid-turn — only the
|
||||
// echo is skipped while streaming so the active turn is not split.
|
||||
// Read-only: every entry point (keyboard, status-bar button) runs
|
||||
// immediately, even mid-turn — only the echo is skipped while streaming
|
||||
// so the active turn is not split.
|
||||
if (!requireActiveSessionForLocalCommand()) return;
|
||||
const owner = sessionOwnerGuard.capture();
|
||||
echoLocalCommandIfIdle(commandText);
|
||||
@@ -13959,23 +13959,11 @@ export function App({
|
||||
t,
|
||||
],
|
||||
);
|
||||
// Stable identity: ChatEditor is memoized and an inline closure would
|
||||
// re-render it on every app render.
|
||||
const handleShowContextUsage = useCallback(
|
||||
() => showContextUsage('/context', false),
|
||||
[showContextUsage],
|
||||
);
|
||||
const contextUsageAvailable = !shouldBlockComposerSubmit({
|
||||
connectionStatus: connection.status,
|
||||
hasSession: Boolean(connection.sessionId),
|
||||
});
|
||||
|
||||
// Stable reference: this travels through the memoized MessageList →
|
||||
// MessageItem chain, so an inline closure would defeat their memo.
|
||||
const handleShowContextDetail = useCallback(() => {
|
||||
showContextUsage('/context detail', true);
|
||||
}, [showContextUsage]);
|
||||
|
||||
const pendingBranchRequestsRef = useRef(new Map<string, Promise<void>>());
|
||||
const branchCurrentSession = useCallback(
|
||||
(options: {
|
||||
@@ -22099,7 +22087,11 @@ export function App({
|
||||
terminalBackgroundShellTaskIds
|
||||
}
|
||||
pendingApproval={pendingToolApproval}
|
||||
onShowContextDetail={handleShowContextDetail}
|
||||
onShowContextDetail={
|
||||
contextUsageAvailable
|
||||
? openCurrentContextUsagePanel
|
||||
: undefined
|
||||
}
|
||||
loadingTranscript={connection.loadingTranscript}
|
||||
catchingUp={connection.catchingUp}
|
||||
hasOlderHistory={transcriptHistory.hasMore}
|
||||
@@ -22876,9 +22868,6 @@ export function App({
|
||||
contextUsageAlwaysVisible={
|
||||
contextUsageAlwaysVisible
|
||||
}
|
||||
onShowContextUsage={
|
||||
contextUsageAvailable ? handleShowContextUsage : undefined
|
||||
}
|
||||
contextUsageControls={
|
||||
connection.sessionId
|
||||
? contextUsageControls[connection.sessionId]
|
||||
|
||||
@@ -1082,7 +1082,27 @@ describe('ChatEditor context usage ring', () => {
|
||||
expect(getContextUsage).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('opens the context breakdown when clicked', () => {
|
||||
it('opens the details panel when clicked', () => {
|
||||
const onOpenContextUsage = vi.fn();
|
||||
const onShowContextUsage = vi.fn();
|
||||
const container = renderChatEditor({
|
||||
tokenCount: 100,
|
||||
contextWindow: 1000,
|
||||
onOpenContextUsage,
|
||||
onShowContextUsage,
|
||||
});
|
||||
|
||||
act(() => {
|
||||
ring(container)!.dispatchEvent(
|
||||
new MouseEvent('click', { bubbles: true, cancelable: true }),
|
||||
);
|
||||
});
|
||||
|
||||
expect(onOpenContextUsage).toHaveBeenCalledTimes(1);
|
||||
expect(onShowContextUsage).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('falls back to a transcript snapshot when the host has no details panel', () => {
|
||||
const onShowContextUsage = vi.fn();
|
||||
const container = renderChatEditor({
|
||||
tokenCount: 100,
|
||||
@@ -1099,6 +1119,24 @@ describe('ChatEditor context usage ring', () => {
|
||||
expect(onShowContextUsage).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('drops the conversation hint once the click opens the details panel', async () => {
|
||||
const container = renderChatEditor({
|
||||
tokenCount: 100,
|
||||
contextWindow: 1000,
|
||||
onOpenContextUsage: vi.fn(),
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
ring(container)!.focus();
|
||||
});
|
||||
|
||||
const tooltip = document.querySelector('[data-web-shell-context-popover]')!;
|
||||
expect(tooltip.textContent).toContain('Context Usage');
|
||||
expect(tooltip.textContent).not.toContain(
|
||||
'Click to view the breakdown in the conversation.',
|
||||
);
|
||||
});
|
||||
|
||||
it('shows the used/total detail in the hover card on focus', async () => {
|
||||
const container = renderChatEditor({
|
||||
tokenCount: 53_600,
|
||||
|
||||
@@ -2119,6 +2119,11 @@ export const ChatEditor = memo(
|
||||
if (!visibleActionSet) return true;
|
||||
return visibleActionSet.has(action);
|
||||
};
|
||||
// A host with no details surface — the embedded side-task pane, which
|
||||
// already lives inside the right panel — keeps the transcript snapshot as
|
||||
// its only way to read the breakdown, and only that case advertises it.
|
||||
const contextRingAction = onOpenContextUsage ?? onShowContextUsage;
|
||||
const contextRingHint = Boolean(onShowContextUsage) && !onOpenContextUsage;
|
||||
const showModeAction = showToolbarAction('approvalMode');
|
||||
const showPlanAction = Boolean(
|
||||
onTogglePlan && visibleActionSet?.has('plan'),
|
||||
@@ -3728,7 +3733,7 @@ export const ChatEditor = memo(
|
||||
contextWindow={contextWindow}
|
||||
controls={contextUsageControls}
|
||||
onOpenDetails={onOpenContextUsage}
|
||||
showSnapshotHint={Boolean(onShowContextUsage)}
|
||||
showSnapshotHint={contextRingHint}
|
||||
>
|
||||
<button
|
||||
className={`${styles.toolBtn} ${styles.contextUsageBtn}`}
|
||||
@@ -3736,9 +3741,9 @@ export const ChatEditor = memo(
|
||||
data-web-shell-context-usage
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onShowContextUsage?.();
|
||||
contextRingAction?.();
|
||||
}}
|
||||
disabled={!onShowContextUsage}
|
||||
disabled={!contextRingAction}
|
||||
aria-label={
|
||||
contextWindow > 0 && tokenCount > 0
|
||||
? t('status.contextUsed', {
|
||||
|
||||
@@ -105,7 +105,15 @@ for (const theme of ['light', 'dark']) {
|
||||
});
|
||||
const ring = page.locator('[data-web-shell-context-usage]');
|
||||
await expect(ring).toHaveAttribute('aria-label', '64.0% context used');
|
||||
await ring.click();
|
||||
// The ring opens the details panel, so the transcript snapshot this
|
||||
// scenario asserts on comes from the slash command instead.
|
||||
const snapshotEditor = page.locator(
|
||||
'[data-web-shell-composer-surface] .cm-content[contenteditable="true"]',
|
||||
);
|
||||
await snapshotEditor.fill('/context');
|
||||
await page
|
||||
.getByRole('button', { name: 'Send message', exact: true })
|
||||
.click();
|
||||
const historical = page
|
||||
.getByRole('group', { name: 'Context Usage', exact: true })
|
||||
.first();
|
||||
|
||||
@@ -214,33 +214,36 @@ for (const theme of ['light', 'dark']) {
|
||||
for (const label of await tooltip.locator('dt').all()) {
|
||||
await expect(label).toHaveCSS('color', secondaryColor);
|
||||
}
|
||||
await expect(tooltip).toContainText(
|
||||
// The main composer has a details panel, so the popover points at that
|
||||
// instead of advertising a transcript snapshot.
|
||||
await expect(tooltip).not.toContainText(
|
||||
'Click to view the breakdown in the conversation.',
|
||||
);
|
||||
await expect(
|
||||
tooltip.getByText('Click to view the breakdown in the conversation.', {
|
||||
exact: true,
|
||||
}),
|
||||
).toHaveCSS('color', secondaryColor);
|
||||
tooltip.getByRole('button', { name: 'View details', exact: true }),
|
||||
).toBeVisible();
|
||||
expect(contextRequests).toEqual([]);
|
||||
await page.screenshot({
|
||||
path: testInfo.outputPath(`context-hover-${theme}.png`),
|
||||
});
|
||||
await usage.click();
|
||||
const history = page.locator('[data-web-shell-message-list]');
|
||||
const cards = history.getByRole('group', {
|
||||
name: 'Context Usage',
|
||||
exact: true,
|
||||
});
|
||||
|
||||
// The transcript detail card is still reachable, through the command. It is
|
||||
// produced before the panel is ever opened, so the overflow checks below
|
||||
// measure a chat column that is not mid-transition.
|
||||
await editor.fill('/context detail');
|
||||
await page
|
||||
.getByRole('button', { name: 'Send message', exact: true })
|
||||
.click();
|
||||
await expect(cards).toHaveCount(1);
|
||||
await expect(cards.first()).toContainText('60.0%');
|
||||
expect(contextRequests).toEqual([false]);
|
||||
await cards.first().getByRole('button', { name: 'View details' }).click();
|
||||
await expect(cards).toHaveCount(2);
|
||||
await expect(
|
||||
history.getByRole('region', { name: 'Context Usage', exact: true }),
|
||||
).toHaveCount(0);
|
||||
expect(contextRequests).toEqual([false, true]);
|
||||
expect(contextRequests).toEqual([true]);
|
||||
const detailCard = cards.last();
|
||||
await expect(detailCard.locator('[class*="total"]')).toHaveText(
|
||||
'60.0k / 100.0k tokens',
|
||||
@@ -397,7 +400,12 @@ for (const theme of ['light', 'dark']) {
|
||||
'background-color',
|
||||
color,
|
||||
);
|
||||
await usage.click();
|
||||
// The ring opens the panel instead of posting a card, so the transcript
|
||||
// reading under test comes from the slash command.
|
||||
await editor.fill('/context');
|
||||
await page
|
||||
.getByRole('button', { name: 'Send message', exact: true })
|
||||
.click();
|
||||
await expect(tooltip).toHaveCount(0);
|
||||
await expect(cards.last().locator('[class*="percentage"]')).toHaveText(
|
||||
`${tokens / 1000}.0%`,
|
||||
|
||||
Reference in new issue
Block a user