mirror of
https://github.com/deepseek-ai/deepseek-harness.git
synced 2026-11-09 20:07:43 +08:00
fix(client): preserve composer position above mobile keyboards
Patch Lexical viewport coordinates and cover keyboard panning in editor and browser regressions. Update the patch disclosure.
This commit is contained in:
@@ -41,6 +41,7 @@ External packages installed for runtime use or embedded in distributed Host or b
|
||||
| [`@earendil-works/pi-ai`](https://github.com/earendil-works/pi) | MIT |
|
||||
| [`@earendil-works/pi-coding-agent`](https://github.com/earendil-works/pi) | MIT |
|
||||
| [`@eslint-community/regexpp`](https://github.com/eslint-community/regexpp) | MIT |
|
||||
| [`@floating-ui/dom`](https://github.com/floating-ui/floating-ui) | MIT |
|
||||
| [`@fortune-sheet/core`](https://github.com/ruilisi/fortune-sheet) | MIT |
|
||||
| [`@fortune-sheet/react`](https://github.com/ruilisi/fortune-sheet) | MIT |
|
||||
| [`@joplin/turndown-plugin-gfm`](https://github.com/laurent22/joplin-turndown-plugin-gfm) | MIT |
|
||||
@@ -159,6 +160,7 @@ pnpm applies local patches to the following packages at install time, so shipped
|
||||
- `@yao-pkg/pkg@6.21.0` — [`patches/@yao-pkg__pkg@6.21.0.patch`](patches/@yao-pkg__pkg@6.21.0.patch)
|
||||
- `app-builder-lib@26.17.0` — [`patches/app-builder-lib@26.17.0.patch`](patches/app-builder-lib@26.17.0.patch)
|
||||
- `exceljs@4.4.0` — [`patches/exceljs@4.4.0.patch`](patches/exceljs@4.4.0.patch)
|
||||
- `lexical@0.49.0` — [`patches/lexical@0.49.0.patch`](patches/lexical@0.49.0.patch)
|
||||
- `node-pty@1.2.0-beta.15` — [`patches/node-pty@1.2.0-beta.15.patch`](patches/node-pty@1.2.0-beta.15.patch)
|
||||
|
||||
## Chrome DevTools frontend
|
||||
|
||||
@@ -39,6 +39,51 @@ function describeSelection(state: Awaited<ReturnType<typeof selectionState>>): s
|
||||
return `${state.focused ? 'focused' : 'unfocused'}; selection ${JSON.stringify(state.text)} (${state.anchor} → ${state.focus})`
|
||||
}
|
||||
|
||||
/**
|
||||
* Keep the visible composer still when keyboard panning changes viewport coordinates.
|
||||
* @param page - Page with the focused composer.
|
||||
* @returns The observed page and composer scroll behavior.
|
||||
*/
|
||||
async function exerciseKeyboardViewport(page: Page): Promise<string> {
|
||||
const input = page.locator('[data-composer-input][contenteditable="true"]')
|
||||
const restore = await page.evaluateHandle(() => {
|
||||
const viewport = window.visualViewport
|
||||
if (viewport === null) throw new Error('Expected a visual viewport')
|
||||
const style = document.documentElement.style
|
||||
const height = style.getPropertyValue('height')
|
||||
const priority = style.getPropertyPriority('height')
|
||||
const scroll = window.scrollY
|
||||
const offset = Object.getOwnPropertyDescriptor(viewport, 'offsetTop')
|
||||
style.setProperty('height', `${window.innerHeight * 2}px`)
|
||||
window.scrollTo(0, window.innerHeight)
|
||||
// Safari can report the keyboard's page displacement as offsetTop even
|
||||
// though DOM rectangles already account for that page scroll.
|
||||
Object.defineProperty(viewport, 'offsetTop', { configurable: true, get: () => window.scrollY })
|
||||
return () => {
|
||||
if (offset === undefined) Reflect.deleteProperty(viewport, 'offsetTop')
|
||||
else Object.defineProperty(viewport, 'offsetTop', offset)
|
||||
if (height === '') style.removeProperty('height')
|
||||
else style.setProperty('height', height, priority)
|
||||
window.scrollTo(0, scroll)
|
||||
}
|
||||
})
|
||||
try {
|
||||
const before = await input.evaluate(el => ({ top: el.getBoundingClientRect().top, scroll: window.scrollY }))
|
||||
expect(before.scroll).toBeGreaterThan(0)
|
||||
await page.keyboard.insertText('z')
|
||||
const after = await input.evaluate(el => new Promise<{ top: number; scroll: number }>((resolve) => {
|
||||
requestAnimationFrame(() => requestAnimationFrame(() => {
|
||||
resolve({ top: el.getBoundingClientRect().top, scroll: window.scrollY })
|
||||
}))
|
||||
}))
|
||||
expect(after).toEqual(before)
|
||||
return '- typing with a keyboard-panned viewport preserves page scroll and composer position'
|
||||
} finally {
|
||||
await restore.evaluate((dispose) => { dispose() })
|
||||
await restore.dispose()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Exercise the card's top, inter-row gap, toolbar gap and bottom padding.
|
||||
* @param page - Page containing an editable composer.
|
||||
@@ -113,6 +158,15 @@ async function exerciseComposer(page: Page): Promise<string[]> {
|
||||
await page.keyboard.insertText('!')
|
||||
await expect.poll(() => input.textContent()).toBe('alpha BET!A gamma')
|
||||
observations.push(`- typing replaces the restored selection and resumes at the restored caret: ${await input.textContent()}`)
|
||||
const retained = await input.elementHandle()
|
||||
const beforeResize = await selectionState(page)
|
||||
for (const width of [390, 768, 1680]) {
|
||||
await page.setViewportSize({ width, height: 1000 })
|
||||
expect(await retained!.evaluate(element => element.isConnected)).toBe(true)
|
||||
expect(await selectionState(page)).toEqual(beforeResize)
|
||||
}
|
||||
observations.push('- resizing through 390px, 768px and 1680px preserves the editor, focus, draft and caret')
|
||||
observations.push(await exerciseKeyboardViewport(page))
|
||||
return observations
|
||||
}
|
||||
|
||||
|
||||
@@ -8,6 +8,8 @@
|
||||
- bottom padding: focused; selection "beta" (10 → 6)
|
||||
- return from outside: focused; selection "beta" (10 → 6)
|
||||
- typing replaces the restored selection and resumes at the restored caret: alpha BET!A gamma
|
||||
- resizing through 390px, 768px and 1680px preserves the editor, focus, draft and caret
|
||||
- typing with a keyboard-panned viewport preserves page scroll and composer position
|
||||
|
||||
## Existing session
|
||||
|
||||
@@ -17,3 +19,5 @@
|
||||
- bottom padding: focused; selection "beta" (10 → 6)
|
||||
- return from outside: focused; selection "beta" (10 → 6)
|
||||
- typing replaces the restored selection and resumes at the restored caret: alpha BET!A gamma
|
||||
- resizing through 390px, 768px and 1680px preserves the editor, focus, draft and caret
|
||||
- typing with a keyboard-panned viewport preserves page scroll and composer position
|
||||
|
||||
@@ -21,8 +21,8 @@
|
||||
en: 9912b286384c41d7
|
||||
zh: 3e994887c1948677
|
||||
/deepseek-ai-dsh-client-ui-conversation/shell-and-standard-props:
|
||||
en: fce20a7d042b1d29
|
||||
zh: 0f8e4faff447df6f
|
||||
en: 74566fff43564bfe
|
||||
zh: 4944512f461a08b0
|
||||
/deepseek-ai-dsh-client-ui-conversation/temporary-composer-entries:
|
||||
en: bb1244310f5c0382
|
||||
zh: 824b84077432b559
|
||||
|
||||
@@ -68,7 +68,7 @@ The composer measures its expanded control groups after size, content, visibilit
|
||||
|
||||
The context meter follows the [statistics-control radius](../../../docs/ui-radius.md#radius-scale).
|
||||
|
||||
The composer uses the shared [text-input focus policy](../ui-primitives/README.md#use-this-package) on mount, Session switching, and unlock. Identified phones and tablets require tapping the input; desktop devices retain automatic focus.
|
||||
The composer uses the shared [text-input focus policy](../ui-primitives/README.md#use-this-package) on mount, Session switching, and unlock. Identified phones and tablets require tapping the input; desktop devices retain automatic focus. The [Lexical patch](../../../patches/lexical@0.49.0.patch) keeps caret scrolling in the same coordinate system as DOM geometry when a mobile keyboard pans the page.
|
||||
|
||||
The context-occupancy button shows a ring and percentage below the input card, after the Session statistics. Clicking it opens the token breakdown in a panel kept inside the viewport, including when no statistics are shown; the button stays hidden until context usage and capacity are available.
|
||||
|
||||
|
||||
@@ -68,7 +68,7 @@ Queue 预览启用空白折叠和文档边缘 trim,保留 emoji,并使用 CS
|
||||
|
||||
上下文占用率入口遵循[统计控件圆角规则](../../../docs/ui-radius.zh.md#radius-scale)。
|
||||
|
||||
composer 在挂载、切换 Session 和解除锁定时使用共享的[文字输入框聚焦策略](../ui-primitives/README.zh.md#use-this-package)。识别出的手机和平板需要点击输入框;桌面设备保留自动聚焦。
|
||||
composer 在挂载、切换 Session 和解除锁定时使用共享的[文字输入框聚焦策略](../ui-primitives/README.zh.md#use-this-package)。识别出的手机和平板需要点击输入框;桌面设备保留自动聚焦。移动端键盘平移页面时,[Lexical 补丁](../../../patches/lexical@0.49.0.patch)让光标滚动计算与 DOM 几何使用同一坐标系。
|
||||
|
||||
上下文占用按钮在输入卡片下方、会话统计右侧显示圆环和百分比。点击按钮可在视口内的面板查看 token 构成,没有统计项时面板也不会越界;上下文用量和容量尚不可用时,按钮保持隐藏。
|
||||
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
// @vitest-environment jsdom
|
||||
/** Composer caret visibility across page scrolling and virtual-keyboard viewport changes. */
|
||||
import { expect, it, onTestFinished, vi } from 'vitest'
|
||||
import { DraftEditorRuntime } from '../src/client/input/editor/runtime.ts'
|
||||
|
||||
function fixture(viewport: { height: number; offsetTop: number; pageTop: number } | undefined, pageScroll: number) {
|
||||
// jsdom has no visual viewport or Range geometry; use the browser measurements
|
||||
// while leaving Lexical's DOM selection and scrolling decisions intact.
|
||||
vi.stubGlobal('visualViewport', viewport)
|
||||
vi.stubGlobal('scrollY', pageScroll)
|
||||
vi.stubGlobal('innerHeight', 600)
|
||||
const scrollBy = vi.spyOn(window, 'scrollBy').mockImplementation(() => {})
|
||||
const geometry = Object.getOwnPropertyDescriptor(Range.prototype, 'getBoundingClientRect')
|
||||
let caret = new DOMRect(20, 169, 0, 26)
|
||||
Object.defineProperty(Range.prototype, 'getBoundingClientRect', {
|
||||
configurable: true,
|
||||
value: () => caret,
|
||||
})
|
||||
const root = document.createElement('div')
|
||||
root.setAttribute('contenteditable', 'true')
|
||||
root.tabIndex = 0
|
||||
root.getBoundingClientRect = () => new DOMRect(0, 0, 400, 1000)
|
||||
document.body.append(root)
|
||||
const runtime: DraftEditorRuntime = new DraftEditorRuntime({
|
||||
onUpdate: () => { runtime.refreshProjection() },
|
||||
openReference: () => false, activeClaimToken: () => null,
|
||||
lexicon: () => new Map(), resolveLexicon: () => undefined,
|
||||
})
|
||||
const dispose = runtime.register()
|
||||
onTestFinished(() => {
|
||||
dispose()
|
||||
root.remove()
|
||||
scrollBy.mockRestore()
|
||||
if (geometry === undefined) Reflect.deleteProperty(Range.prototype, 'getBoundingClientRect')
|
||||
else Object.defineProperty(Range.prototype, 'getBoundingClientRect', geometry)
|
||||
vi.unstubAllGlobals()
|
||||
})
|
||||
runtime.editor.setRootElement(root)
|
||||
root.focus({ preventScroll: true })
|
||||
scrollBy.mockClear()
|
||||
return {
|
||||
scrollBy,
|
||||
typeAt(top: number): void {
|
||||
caret = new DOMRect(20, top, 0, 26)
|
||||
runtime.setDraft('a')
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
it('keeps a visible caret still after Safari pans the page above the keyboard', () => {
|
||||
const f = fixture({ height: 376, offsetTop: 196, pageTop: 196 }, 196)
|
||||
f.typeAt(169)
|
||||
expect(f.scrollBy).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('reveals a caret below the keyboard after the page has scrolled', () => {
|
||||
const f = fixture({ height: 376, offsetTop: 196, pageTop: 196 }, 196)
|
||||
f.typeAt(410)
|
||||
expect(f.scrollBy).toHaveBeenCalledExactlyOnceWith(0, 60)
|
||||
})
|
||||
|
||||
it('retains visual viewport panning relative to an already scrolled page', () => {
|
||||
const f = fixture({ height: 376, offsetTop: 150, pageTop: 350 }, 200)
|
||||
f.typeAt(100)
|
||||
expect(f.scrollBy).toHaveBeenCalledExactlyOnceWith(0, -50)
|
||||
})
|
||||
|
||||
it('uses the window height when the browser has no visual viewport', () => {
|
||||
const f = fixture(undefined, 200)
|
||||
f.typeAt(630)
|
||||
expect(f.scrollBy).toHaveBeenCalledExactlyOnceWith(0, 56)
|
||||
})
|
||||
File diff suppressed because one or more lines are too long
Generated
+14
-13
@@ -18,6 +18,7 @@ patchedDependencies:
|
||||
'@yao-pkg/pkg@6.21.0': 4d4861969595629311f7d90a0b34b15481c96d5682d828a151a104c69d4616d3
|
||||
app-builder-lib@26.17.0: b70dc2a2a78ffabb80a4f14fafc7c103237f2b0a207e1f296fa1c1387ea29348
|
||||
exceljs@4.4.0: c1bf8ac8f319dab382f08e3a669b715e691d46c703ac5a4163eb383fbedd1d48
|
||||
lexical@0.49.0: 54c4413db208cf234cb68c6a837f2523d3b319aa17911f1da385af08362e1356
|
||||
node-pty@1.2.0-beta.15: 9d30053266d0f39d3debe894c675867f6782da6bcadbc94a693b3cfb9eff8ed0
|
||||
|
||||
importers:
|
||||
@@ -3993,7 +3994,7 @@ importers:
|
||||
version: 2.1.1
|
||||
lexical:
|
||||
specifier: ^0.49.0
|
||||
version: 0.49.0(typescript@6.0.3)
|
||||
version: 0.49.0(patch_hash=54c4413db208cf234cb68c6a837f2523d3b319aa17911f1da385af08362e1356)(typescript@6.0.3)
|
||||
react:
|
||||
specifier: ^18.2.0
|
||||
version: 18.3.1
|
||||
@@ -25690,14 +25691,14 @@ snapshots:
|
||||
'@lexical/selection': 0.49.0(typescript@6.0.3)
|
||||
'@lexical/utils': 0.49.0(typescript@6.0.3)
|
||||
'@types/trusted-types': 2.0.7
|
||||
lexical: 0.49.0(typescript@6.0.3)
|
||||
lexical: 0.49.0(patch_hash=54c4413db208cf234cb68c6a837f2523d3b319aa17911f1da385af08362e1356)(typescript@6.0.3)
|
||||
optionalDependencies:
|
||||
typescript: 6.0.3
|
||||
|
||||
'@lexical/dragon@0.49.0(typescript@6.0.3)':
|
||||
dependencies:
|
||||
'@lexical/extension': 0.49.0(typescript@6.0.3)
|
||||
lexical: 0.49.0(typescript@6.0.3)
|
||||
lexical: 0.49.0(patch_hash=54c4413db208cf234cb68c6a837f2523d3b319aa17911f1da385af08362e1356)(typescript@6.0.3)
|
||||
optionalDependencies:
|
||||
typescript: 6.0.3
|
||||
|
||||
@@ -25706,7 +25707,7 @@ snapshots:
|
||||
'@lexical/internal': 0.49.0(typescript@6.0.3)
|
||||
'@lexical/utils': 0.49.0(typescript@6.0.3)
|
||||
'@preact/signals-core': 1.14.4
|
||||
lexical: 0.49.0(typescript@6.0.3)
|
||||
lexical: 0.49.0(patch_hash=54c4413db208cf234cb68c6a837f2523d3b319aa17911f1da385af08362e1356)(typescript@6.0.3)
|
||||
optionalDependencies:
|
||||
typescript: 6.0.3
|
||||
|
||||
@@ -25714,7 +25715,7 @@ snapshots:
|
||||
dependencies:
|
||||
'@lexical/internal': 0.49.0(typescript@6.0.3)
|
||||
happy-dom: 20.11.6
|
||||
lexical: 0.49.0(typescript@6.0.3)
|
||||
lexical: 0.49.0(patch_hash=54c4413db208cf234cb68c6a837f2523d3b319aa17911f1da385af08362e1356)(typescript@6.0.3)
|
||||
optionalDependencies:
|
||||
typescript: 6.0.3
|
||||
transitivePeerDependencies:
|
||||
@@ -25725,7 +25726,7 @@ snapshots:
|
||||
dependencies:
|
||||
'@lexical/extension': 0.49.0(typescript@6.0.3)
|
||||
'@lexical/utils': 0.49.0(typescript@6.0.3)
|
||||
lexical: 0.49.0(typescript@6.0.3)
|
||||
lexical: 0.49.0(patch_hash=54c4413db208cf234cb68c6a837f2523d3b319aa17911f1da385af08362e1356)(typescript@6.0.3)
|
||||
optionalDependencies:
|
||||
typescript: 6.0.3
|
||||
|
||||
@@ -25735,7 +25736,7 @@ snapshots:
|
||||
'@lexical/internal': 0.49.0(typescript@6.0.3)
|
||||
'@lexical/selection': 0.49.0(typescript@6.0.3)
|
||||
'@lexical/utils': 0.49.0(typescript@6.0.3)
|
||||
lexical: 0.49.0(typescript@6.0.3)
|
||||
lexical: 0.49.0(patch_hash=54c4413db208cf234cb68c6a837f2523d3b319aa17911f1da385af08362e1356)(typescript@6.0.3)
|
||||
optionalDependencies:
|
||||
typescript: 6.0.3
|
||||
|
||||
@@ -25749,7 +25750,7 @@ snapshots:
|
||||
'@lexical/html': 0.49.0(typescript@6.0.3)
|
||||
'@lexical/internal': 0.49.0(typescript@6.0.3)
|
||||
'@lexical/utils': 0.49.0(typescript@6.0.3)
|
||||
lexical: 0.49.0(typescript@6.0.3)
|
||||
lexical: 0.49.0(patch_hash=54c4413db208cf234cb68c6a837f2523d3b319aa17911f1da385af08362e1356)(typescript@6.0.3)
|
||||
optionalDependencies:
|
||||
typescript: 6.0.3
|
||||
|
||||
@@ -25760,21 +25761,21 @@ snapshots:
|
||||
'@lexical/extension': 0.49.0(typescript@6.0.3)
|
||||
'@lexical/selection': 0.49.0(typescript@6.0.3)
|
||||
'@lexical/utils': 0.49.0(typescript@6.0.3)
|
||||
lexical: 0.49.0(typescript@6.0.3)
|
||||
lexical: 0.49.0(patch_hash=54c4413db208cf234cb68c6a837f2523d3b319aa17911f1da385af08362e1356)(typescript@6.0.3)
|
||||
optionalDependencies:
|
||||
typescript: 6.0.3
|
||||
|
||||
'@lexical/selection@0.49.0(typescript@6.0.3)':
|
||||
dependencies:
|
||||
'@lexical/internal': 0.49.0(typescript@6.0.3)
|
||||
lexical: 0.49.0(typescript@6.0.3)
|
||||
lexical: 0.49.0(patch_hash=54c4413db208cf234cb68c6a837f2523d3b319aa17911f1da385af08362e1356)(typescript@6.0.3)
|
||||
optionalDependencies:
|
||||
typescript: 6.0.3
|
||||
|
||||
'@lexical/text@0.49.0(typescript@6.0.3)':
|
||||
dependencies:
|
||||
'@lexical/internal': 0.49.0(typescript@6.0.3)
|
||||
lexical: 0.49.0(typescript@6.0.3)
|
||||
lexical: 0.49.0(patch_hash=54c4413db208cf234cb68c6a837f2523d3b319aa17911f1da385af08362e1356)(typescript@6.0.3)
|
||||
optionalDependencies:
|
||||
typescript: 6.0.3
|
||||
|
||||
@@ -25782,7 +25783,7 @@ snapshots:
|
||||
dependencies:
|
||||
'@lexical/internal': 0.49.0(typescript@6.0.3)
|
||||
'@lexical/selection': 0.49.0(typescript@6.0.3)
|
||||
lexical: 0.49.0(typescript@6.0.3)
|
||||
lexical: 0.49.0(patch_hash=54c4413db208cf234cb68c6a837f2523d3b319aa17911f1da385af08362e1356)(typescript@6.0.3)
|
||||
optionalDependencies:
|
||||
typescript: 6.0.3
|
||||
|
||||
@@ -29646,7 +29647,7 @@ snapshots:
|
||||
prelude-ls: 1.2.1
|
||||
type-check: 0.4.0
|
||||
|
||||
lexical@0.49.0(typescript@6.0.3):
|
||||
lexical@0.49.0(patch_hash=54c4413db208cf234cb68c6a837f2523d3b319aa17911f1da385af08362e1356)(typescript@6.0.3):
|
||||
dependencies:
|
||||
'@lexical/internal': 0.49.0(typescript@6.0.3)
|
||||
optionalDependencies:
|
||||
|
||||
@@ -116,4 +116,5 @@ patchedDependencies:
|
||||
'@yao-pkg/pkg@6.21.0': patches/@yao-pkg__pkg@6.21.0.patch
|
||||
app-builder-lib@26.17.0: patches/app-builder-lib@26.17.0.patch
|
||||
exceljs@4.4.0: patches/exceljs@4.4.0.patch
|
||||
lexical@0.49.0: patches/lexical@0.49.0.patch
|
||||
node-pty@1.2.0-beta.15: patches/node-pty@1.2.0-beta.15.patch
|
||||
|
||||
Reference in New Issue
Block a user