Merge pull request #3155 from deepseek-harness/feat/web-input-trigger-menu-polish

feat(web): 输入触发菜单(/ 与 @)呈现打磨
This commit is contained in:
Yichen Jiang
2026-08-27 10:12:39 +08:00
committed by GitHub
100 changed files with 496 additions and 299 deletions
@@ -21,7 +21,7 @@ async function freshComposer(): Promise<HTMLElement> {
fireEvent.click(start)
return await waitFor(() => {
const surface = document.querySelector<HTMLElement>(
'[data-composer-input][data-placeholder="Describe what you want to build"]',
'[data-composer-input][data-placeholder="Describe what you want to build... / commands, @ files or sessions"]',
)
if (surface === null) throw new Error('composer surface missing')
return surface
@@ -42,7 +42,7 @@
- button "Branch into a new conversation":
- img
- text: {{clock}} Ran for {{duration}}
- textbox "Message the agent"
- textbox "Message or run a task... / commands, @ files or sessions"
- button "Commands":
- img
- 'button "Access mode, current: Read Only"': Read Only
@@ -14,7 +14,7 @@
- img
- img
- text: "goal No goal is currently set. Usage: /goal [<objective>|clear|edit <objective>|pause|resume]"
- textbox "Message the agent"
- textbox "Message or run a task... / commands, @ files or sessions"
- button "Commands":
- img
- 'button "Access mode, current: Workspace Write"': Workspace Write
@@ -45,7 +45,7 @@
- button "Branch into a new conversation":
- img
- text: {{clock}} Ran for {{duration}}
- textbox "Message the agent"
- textbox "Message or run a task... / commands, @ files or sessions"
- button "Commands":
- img
- 'button "Access mode, current: Workspace Write"': Workspace Write
@@ -24,7 +24,7 @@
- button "Branch into a new conversation":
- img
- text: {{clock}} Ran for {{duration}}
- textbox "Message the agent"
- textbox "Message or run a task... / commands, @ files or sessions"
- button "Commands":
- img
- 'button "Access mode, current: Workspace Write"': Workspace Write
@@ -36,7 +36,7 @@
- button "Branch into a new conversation":
- img
- text: {{clock}} Ran for {{duration}}
- textbox "Message the agent"
- textbox "Message or run a task... / commands, @ files or sessions"
- button "Commands":
- img
- 'button "Access mode, current: Workspace Write"': Workspace Write
@@ -40,7 +40,7 @@
- button "Branch into a new conversation":
- img
- text: {{clock}} Ran for {{duration}}
- textbox "Message the agent"
- textbox "Message or run a task... / commands, @ files or sessions"
- button "Commands":
- img
- 'button "Access mode, current: Workspace Write"': Workspace Write
@@ -1,9 +1,10 @@
- listbox "Trigger suggestions":
- text: Files & folders
- option "Folder · folderx/ folderx Browse folder" [selected]:
- text: Folder · folderx/ folderx
- button "Browse folder":
- option "File · reference.txt reference.txt"
- text: Session conversations
- option "Session · Reference order target reference-order-target-session · {{cwd}} · {{timestamp}}"
- option "Session · Research notes reference-source-session · {{cwd}} · {{timestamp}}"
- option "folderx/ folderx Browse folder" [selected]:
- text: folderx/ folderx
- button "Browse folder":
- img
- option "reference.txt reference.txt"
- text: Sessions
- option "Reference order target reference-order-target-session · {{cwd}} · {{timestamp}}"
- option "Research notes reference-source-session · {{cwd}} · {{timestamp}}"
@@ -13,7 +13,7 @@
- button "Session recall Research notes":
- img
- text: Session recall Research notes
- textbox "Message the agent"
- textbox "Message or run a task... / commands, @ files or sessions"
- button "Commands":
- img
- 'button "Access mode, current: Workspace Write"': Workspace Write
@@ -34,7 +34,7 @@
- button "Branch into a new conversation":
- img
- text: {{clock}} Ran for {{duration}} TTFT {{duration}} {{throughput}} tok/s
- textbox "Message the agent"
- textbox "Message or run a task... / commands, @ files or sessions"
- button "Commands":
- img
- 'button "Access mode, current: Workspace Write"': Workspace Write
@@ -375,7 +375,7 @@
- text: 7/25 {{clock}} Ran for {{duration}}
- button "Back to bottom":
- img
- textbox "Message the agent"
- textbox "Message or run a task... / commands, @ files or sessions"
- button "Commands":
- img
- 'button "Access mode, current: Workspace Write"': Workspace Write
@@ -31,7 +31,7 @@
- text: "Interjection: include the word ORANGE in your final reply."
- button "Copy":
- img
- textbox "Message the agent"
- textbox "Message or run a task... / commands, @ files or sessions"
- button "Commands":
- img
- 'button "Access mode, current: Workspace Write"': Workspace Write
@@ -44,7 +44,7 @@
- button "Branch into a new conversation":
- img
- text: {{clock}} Ran for {{duration}} TTFT {{duration}} {{throughput}} tok/s
- textbox "Message the agent"
- textbox "Message or run a task... / commands, @ files or sessions"
- button "Commands":
- img
- 'button "Access mode, current: Workspace Write"': Workspace Write
+1 -1
View File
@@ -45,7 +45,7 @@ describe('web e2e: goal bar clear convergence', () => {
onTestFailed(() => saveFailureShot(page, 'web-e2e-goal-bar-clear'))
// Startup reuses the fixture workspace's blank session, keeping this
// command independent of alpha's running replay and pending question.
const input = page.locator('[data-composer-input][data-placeholder="Describe what you want to build"]')
const input = page.locator('[data-composer-input][data-placeholder="Describe what you want to build... / commands, @ files or sessions"]')
await input.waitFor({ timeout: 10_000 })
await input.fill('/goal guard rapid clear clicks')
await input.press('Enter')
+3 -3
View File
@@ -90,7 +90,7 @@ it('accepts pasted images into the composer rail in order and removes them', asy
// this assembled lane pins the intake chain over the built graph.
const textarea = await waitFor(() => {
const surface = document.querySelector<HTMLElement>(
'[data-composer-input][data-placeholder="Describe what you want to build"]',
'[data-composer-input][data-placeholder="Describe what you want to build... / commands, @ files or sessions"]',
)
if (surface === null) throw new Error('composer surface missing')
return surface
@@ -165,7 +165,7 @@ it('accepts a whole-page drop under the limits-labeled overlay and refuses an ov
fireEvent.click(start)
const textarea = await waitFor(() => {
const surface = document.querySelector<HTMLElement>(
'[data-composer-input][data-placeholder="Describe what you want to build"]',
'[data-composer-input][data-placeholder="Describe what you want to build... / commands, @ files or sessions"]',
)
if (surface === null) throw new Error('composer surface missing')
return surface
@@ -220,7 +220,7 @@ it('renders a host dimension rejection with the projected 2000px limit', async (
const textarea = await waitFor(() => {
const surface = document.querySelector<HTMLElement>(
'[data-composer-input][data-placeholder="Describe what you want to build"]',
'[data-composer-input][data-placeholder="Describe what you want to build... / commands, @ files or sessions"]',
)
if (surface === null) throw new Error('composer surface missing')
return surface
+1 -1
View File
@@ -310,7 +310,7 @@ async function bootPreview(origin: string, browser: Browser): Promise<void> {
const configureLater = page.getByRole('button', { name: 'Configure later' })
await configureLater.waitFor({ timeout: 30_000 })
await configureLater.click()
await page.locator('[data-composer-input][data-placeholder="Describe what you want to build"]')
await page.locator('[data-composer-input][data-placeholder="Describe what you want to build... / commands, @ files or sessions"]')
.waitFor({ timeout: 30_000 })
const exercised = await page.evaluate(async () => {
+13 -13
View File
@@ -150,14 +150,14 @@ describe.skipIf(MODE === 'record')('web e2e: file and session references through
const snapshot = await captureStableAria(page, '[role="listbox"]', scaffold.workspaceCwd)
await compareOrRefreshGolden(MENU_EXPECTED, snapshot, MODE)
expect(snapshot).toContain('Files & folders')
expect(snapshot).toContain('Session conversations')
expect(snapshot).toContain('Sessions')
expect(snapshot).not.toContain('text: reference Files & folders')
expect(snapshot).toContain('File \u00b7 reference.txt')
expect(snapshot).toContain('Session \u00b7 Research notes')
expect(snapshot).toContain('reference.txt')
expect(snapshot).toContain('Research notes')
expect(snapshot).not.toContain('text: Subagents')
await input.fill('@reference')
await menu.getByRole('option', { name: /File \u00b7 reference\.txt/ }).click()
await menu.getByRole('option', { name: /reference\.txt/ }).click()
// The pick lands an atomic chip: a real DOM capsule carrying the domain
// icon and the label (the canonical reference text lives on the node and
// expands on submit; the surface text is the label plus the separator).
@@ -167,7 +167,7 @@ describe.skipIf(MODE === 'record')('web e2e: file and session references through
await expect.poll(() => input.textContent()).toBe('reference.txt ')
await input.fill('@Research')
await menu.getByRole('option', { name: /Session \u00b7 Research notes/ }).click()
await menu.getByRole('option', { name: /Research notes/ }).click()
const sessionReference = page.locator('[data-composer-chip]').last()
await expect.poll(() => sessionReference.textContent()).toBe('Research notes')
await expect.poll(() => sessionReference.locator('svg').count()).toBe(1)
@@ -183,7 +183,7 @@ describe.skipIf(MODE === 'record')('web e2e: file and session references through
const menu = page.getByRole('listbox', { name: 'Trigger suggestions' })
await input.fill('@reference')
await menu.getByRole('option', { name: /File · reference\.txt/ }).click()
await menu.getByRole('option', { name: /reference\.txt/ }).click()
await expect.poll(() => input.locator('[data-composer-chip]').count()).toBe(1)
// The #2813 gesture: collapse the caret to the document start, directly
@@ -192,7 +192,7 @@ describe.skipIf(MODE === 'record')('web e2e: file and session references through
await page.keyboard.press('ControlOrMeta+A')
await page.keyboard.press('ArrowLeft')
await page.keyboard.type('@Research')
await menu.getByRole('option', { name: /Session · Research notes/ }).click()
await menu.getByRole('option', { name: /Research notes/ }).click()
// Both chips survive the boundary insert: the session chip lands ahead of
// the intact file chip.
@@ -212,7 +212,7 @@ describe.skipIf(MODE === 'record')('web e2e: file and session references through
const menu = page.getByRole('listbox', { name: 'Trigger suggestions' })
await input.fill('@reference')
await menu.getByRole('option', { name: /File · reference\.txt/ }).click()
await menu.getByRole('option', { name: /reference\.txt/ }).click()
await expect.poll(() => input.locator('[data-composer-chip]').count()).toBe(1)
// First ArrowLeft crosses the trailing space; the second steps across the
@@ -250,7 +250,7 @@ describe.skipIf(MODE === 'record')('web e2e: file and session references through
// as an atomic chip — folder glyph, no trigger character, one unit.
await writeComposerDraft(page, input, '@folderx')
// First folder query on this page: allow the Host index a cold start.
await menu.getByRole('option', { name: /Folder · folderx\// }).waitFor({ timeout: 60_000 })
await menu.getByRole('option', { name: /^folderx\// }).waitFor({ timeout: 60_000 })
await page.keyboard.press('Enter')
const chip = input.locator('[data-composer-chip]').last()
await expect.poll(() => chip.textContent()).toBe('folderx/')
@@ -260,18 +260,18 @@ describe.skipIf(MODE === 'record')('web e2e: file and session references through
// Tab drills: the literal descent text stays editable and the open menu
// lists the folder's children.
await writeComposerDraft(page, input, '@folderx')
await menu.getByRole('option', { name: /Folder · folderx\// }).waitFor()
await menu.getByRole('option', { name: /^folderx\// }).waitFor()
await page.keyboard.press('Tab')
await expect.poll(() => input.textContent()).toBe('@folderx/')
await menu.getByRole('option', { name: /File · child\.txt/ }).waitFor()
await menu.getByRole('option', { name: /child\.txt/ }).waitFor()
// The row chevron drills the same way by pointer.
await writeComposerDraft(page, input, '@folderx')
const row = menu.getByRole('option', { name: /Folder · folderx\// })
const row = menu.getByRole('option', { name: /^folderx\// })
await row.waitFor()
await row.getByRole('button', { name: 'Browse folder' }).click()
await expect.poll(() => input.textContent()).toBe('@folderx/')
await menu.getByRole('option', { name: /File · child\.txt/ }).waitFor()
await menu.getByRole('option', { name: /child\.txt/ }).waitFor()
await page.keyboard.press('Escape')
expect(tripwire.pageErrors).toEqual([])
@@ -24,7 +24,7 @@
- button "Copy"
- code: "const first: number = 1 const second = \"two\" let tail"
- status: Deep diving...
- textbox "Message the agent"
- textbox "Message or run a task... / commands, @ files or sessions"
- button "Commands":
- img
- 'button "Access mode, current: Workspace Write"': Workspace Write
+1 -1
View File
@@ -130,7 +130,7 @@ describe('web e2e: startup auto-selection', () => {
expect(await page.locator('[data-composer-input]').first().isVisible()).toBe(true)
releaseOpening()
await page.locator('[data-composer-input][contenteditable="true"][data-placeholder="Describe what you want to build"]')
await page.locator('[data-composer-input][contenteditable="true"][data-placeholder="Describe what you want to build... / commands, @ files or sessions"]')
.waitFor({ timeout: 15_000 })
acknowledgeReloadConnectionLoss(tripwire, warningsBefore)
+3 -3
View File
@@ -393,7 +393,7 @@ describe('web e2e: persisted subagent conversation and human continuation', () =
expect(await page.locator('[data-composer-seat]').evaluate(element =>
getComputedStyle(element).visibility)).toBe('hidden')
releaseCatalog()
const input = page.getByRole('textbox', { name: 'Message the agent' })
const input = page.getByRole('textbox', { name: 'Message or run a task... / commands, @ files or sessions' })
await input.waitFor({ timeout: 15_000 })
await expect.poll(() => input.isEnabled(), { timeout: 15_000 }).toBe(true)
acknowledgeReloadConnectionLoss(tripwire, warningStart)
@@ -417,7 +417,7 @@ describe('web e2e: persisted subagent conversation and human continuation', () =
resolveEnded()
})
})
const input = page.getByRole('textbox', { name: 'Message the agent' })
const input = page.getByRole('textbox', { name: 'Message or run a task... / commands, @ files or sessions' })
await input.fill(FOLLOWUP)
await input.press('Enter')
await expect.poll(
@@ -500,7 +500,7 @@ describe('web e2e: persisted subagent conversation and human continuation', () =
.click()
await page.getByRole('button', { name: '3 subagents' }).hover()
await page.getByRole('treeitem', { name: new RegExp(LABEL) }).click()
await page.getByRole('textbox', { name: 'Message the agent' }).waitFor()
await page.getByRole('textbox', { name: 'Message or run a task... / commands, @ files or sessions' }).waitFor()
const forkResponse = page.waitForResponse(response =>
new URL(response.url()).pathname === '/api/session/fork')
await page.getByRole('button', { name: 'Branch into a new conversation' }).last().click()
+1 -1
View File
@@ -259,7 +259,7 @@ describe.skipIf(MODE === 'record')('web e2e: composer interrupt for a running co
.getByRole('button').first().click()
await page.getByRole('button', { name: /1 subagent/ }).click()
await page.getByRole('treeitem', { name: new RegExp(LABEL) }).click()
const input = page.getByRole('textbox', { name: 'Message the agent' })
const input = page.getByRole('textbox', { name: 'Message or run a task... / commands, @ files or sessions' })
await input.waitFor({ timeout: 15_000 })
expect(await input.isDisabled()).toBe(false)
+2 -2
View File
@@ -83,7 +83,7 @@ export async function connectFreshWorkspace(page: Page, root: string, name = 'wo
await dialog.getByRole('button', { name: 'Open', exact: true }).click()
// The pick connected the workspace: the blank session's live composer
// replaces the locked placeholder and enables.
await page.locator('[data-composer-input][contenteditable="true"][data-placeholder="Describe what you want to build"]')
await page.locator('[data-composer-input][contenteditable="true"][data-placeholder="Describe what you want to build... / commands, @ files or sessions"]')
.waitFor({ timeout: 15_000 })
}
@@ -106,7 +106,7 @@ export async function connectFreshWorkspaceZh(page: Page, root: string, name = '
await pathInput.fill(join(root, name))
await pathInput.press('Enter')
await dialog.getByRole('button', { name: '打开', exact: true }).click()
await page.locator('[data-composer-input][contenteditable="true"][data-placeholder="描述你想要构建的内容"]')
await page.locator('[data-composer-input][contenteditable="true"][data-placeholder="描述你想要构建的内容… / 调用指令 @ 文件或会话"]')
.waitFor({ timeout: 15_000 })
}