mirror of
https://github.com/deepseek-ai/deepseek-harness.git
synced 2026-08-29 04:26:38 +00:00
Merge pull request #3155 from deepseek-harness/feat/web-input-trigger-menu-polish
feat(web): 输入触发菜单(/ 与 @)呈现打磨
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 () => {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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 })
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user