mirror of
https://github.com/stablyai/orca.git
synced 2026-11-02 01:57:57 +00:00
* fix(renderer): restore focus after floating tab rename * fix: limit one-shot chat composer focus to tab rename return Other onlyIfFocusUnclaimed callers (e.g. floating terminal) keep main's replayable composer focus request. --------- Co-authored-by: MimeLyc <11363886+MimeLyc@users.noreply.github.com> Co-authored-by: Neil <neil@stably.ai>
87 lines
3.5 KiB
TypeScript
87 lines
3.5 KiB
TypeScript
// Completed tab and composer actions return keyboard input to the chat draft.
|
|
|
|
import type { Locator, Page } from '@stablyai/playwright-test'
|
|
import { expect } from './helpers/orca-app'
|
|
import { waitForActiveWorktree, waitForSessionReady } from './helpers/store'
|
|
import { buildFakeAgentCommandOverride } from './helpers/fake-agent-command-override'
|
|
import { SUPPORTED, createChatOnHost, test } from './helpers/structured-claude-stub'
|
|
|
|
function focusIsIn(composer: Locator): Promise<boolean> {
|
|
return composer.evaluate((element) => element.contains(element.ownerDocument.activeElement))
|
|
}
|
|
|
|
async function openChat(page: Page, claudeCommand: string) {
|
|
await waitForSessionReady(page)
|
|
const worktreeId = await waitForActiveWorktree(page)
|
|
await page.evaluate(async (command) => {
|
|
await window.__store?.getState().updateSettings({
|
|
experimentalNativeChat: true,
|
|
defaultTuiAgent: 'claude',
|
|
agentCmdOverrides: { claude: command }
|
|
})
|
|
}, buildFakeAgentCommandOverride(claudeCommand))
|
|
const sessionId = await createChatOnHost(page, worktreeId)
|
|
const readTabId = () =>
|
|
page.evaluate(
|
|
({ worktreeId, sessionId }) =>
|
|
window.__store
|
|
?.getState()
|
|
.unifiedTabsByWorktree[worktreeId]?.find((tab) => tab.entityId === sessionId)?.id,
|
|
{ worktreeId, sessionId }
|
|
)
|
|
await expect.poll(readTabId, { timeout: 30_000 }).toBeTruthy()
|
|
const tab = page.locator(`[data-testid="sortable-tab"][data-tab-id="${await readTabId()}"]`)
|
|
await tab.click()
|
|
const chat = page.locator('[data-native-chat-root="true"]')
|
|
const composer = chat.getByRole('textbox')
|
|
await composer.click()
|
|
return {
|
|
tab,
|
|
composer,
|
|
send: chat.getByRole('button', { name: 'Send', exact: true }),
|
|
stop: chat.getByRole('button', { name: 'Stop the agent' }),
|
|
reply: chat.getByText('Stub reply.')
|
|
}
|
|
}
|
|
|
|
test.describe('native chat composer focus', () => {
|
|
test.skip(!SUPPORTED, 'the Claude stub wrapper is POSIX-only')
|
|
|
|
test('keyboard tab rename returns input to the native chat composer', async ({
|
|
orcaPage: page,
|
|
claudeStub
|
|
}) => {
|
|
const { tab, composer } = await openChat(page, claudeStub.command)
|
|
await tab.dblclick()
|
|
const rename = tab.locator('[data-tab-rename-input="true"]')
|
|
await expect(rename).toBeFocused()
|
|
await rename.fill('')
|
|
await rename.pressSequentially('Renamed chat', { delay: 60 })
|
|
await page.keyboard.press('Enter')
|
|
await expect(rename).toHaveCount(0)
|
|
await expect(tab).toHaveAttribute('data-tab-title', 'Renamed chat')
|
|
await expect.poll(() => focusIsIn(composer)).toBe(true)
|
|
await page.keyboard.type('Draft after rename')
|
|
await expect(composer).toContainText('Draft after rename')
|
|
})
|
|
|
|
test('when Send disables at the end of the turn', async ({ orcaPage: page, claudeStub }) => {
|
|
const { composer, send, stop, reply } = await openChat(page, claudeStub.command)
|
|
await page.keyboard.type('hello')
|
|
await send.click()
|
|
await expect(reply).toBeVisible({ timeout: 30_000 })
|
|
await expect(stop).toHaveCount(0)
|
|
await expect.poll(() => focusIsIn(composer)).toBe(true)
|
|
})
|
|
|
|
test('when Stop disables while the stop is in flight', async ({ orcaPage: page, claudeStub }) => {
|
|
const { composer, send, stop } = await openChat(page, claudeStub.command)
|
|
// The stub runs a prompt containing "slow" until it is interrupted.
|
|
await page.keyboard.type('slow')
|
|
await send.click()
|
|
await stop.click()
|
|
await expect(stop).toHaveCount(0, { timeout: 30_000 })
|
|
await expect.poll(() => focusIsIn(composer)).toBe(true)
|
|
})
|
|
})
|