mirror of
https://github.com/anomalyco/opencode.git
synced 2026-11-13 19:46:16 +03:00
fix(app): focus the composer when switching session tabs (#56273)
This commit is contained in:
committed by
GitHub
Unverified
parent
7a636bb737
commit
07b53425c7
@@ -12,7 +12,7 @@ for (const custom of [false, true]) {
|
||||
await installStressSessionTabs(page)
|
||||
|
||||
// The legacy command ID; the keybind migration renames it to `details.toggle`.
|
||||
if (custom) await seed(page, { settings: { keybinds: { "session.summary.toggle": "f8" } } })
|
||||
if (custom) await seed(page, { settings: { keybinds: { "session.summary.toggle": "alt+f8" } } })
|
||||
await page.goto(sessionHref(fixture.sourceID))
|
||||
const trigger = page.getByRole("button", { name: "Session details", exact: true })
|
||||
const summary = page.getByRole("dialog", { name: "Session details", exact: true })
|
||||
@@ -27,9 +27,9 @@ for (const custom of [false, true]) {
|
||||
const tooltip = page.getByRole("tooltip")
|
||||
await expect(tooltip).toContainText("Summary")
|
||||
const mac = await page.evaluate(() => /(Mac|iPod|iPhone|iPad)/.test(navigator.platform))
|
||||
const shortcut = custom ? "F8" : mac ? "Meta+Shift+Y" : "Control+Shift+Y"
|
||||
const shortcut = custom ? "Alt+F8" : mac ? "Meta+Shift+Y" : "Control+Shift+Y"
|
||||
await expect(tooltip.locator('[data-slot="keybind-v2-label"]')).toHaveText(
|
||||
custom ? ["F8"] : mac ? ["⇧", "⌘", "Y"] : ["Ctrl", "Shift", "Y"],
|
||||
custom ? (mac ? ["⌥", "F8"] : ["Alt", "F8"]) : mac ? ["⇧", "⌘", "Y"] : ["Ctrl", "Shift", "Y"],
|
||||
)
|
||||
|
||||
for (const id of [fixture.sourceID, fixture.targetID, fixture.sourceID]) {
|
||||
@@ -53,13 +53,13 @@ for (const custom of [false, true]) {
|
||||
|
||||
// The override moved, so storage keeps only its new id and Shortcuts lists no row titled with the old one.
|
||||
const keybinds = () => page.evaluate(() => JSON.parse(localStorage.getItem("settings.v3") ?? "null")?.keybinds)
|
||||
await expect.poll(keybinds).toEqual({ "details.toggle": "f8" })
|
||||
await expect.poll(keybinds).toEqual({ "details.toggle": "alt+f8" })
|
||||
await page.goto("/settings")
|
||||
const settings = page.getByTestId("settings-screen")
|
||||
await settings.getByRole("tab", { name: "Shortcuts", exact: true }).click()
|
||||
await expect(settings.getByText("Toggle summary", { exact: true })).toBeVisible()
|
||||
await expect(settings.getByText("session.summary.toggle", { exact: true })).toHaveCount(0)
|
||||
expect(await keybinds()).toEqual({ "details.toggle": "f8" })
|
||||
expect(await keybinds()).toEqual({ "details.toggle": "alt+f8" })
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -125,10 +125,10 @@ test("inactive session attention dots share the close control without marking th
|
||||
test("a tab does not reopen its title editor while a rename is saving", async ({ page }) => {
|
||||
await mockWorkspace(page, { name: "Tabs", sessions: [a, b] })
|
||||
await page.goto(sessionHref(a.id))
|
||||
const title = page.locator(`[data-titlebar-tab-slot]:has(a[href="${sessionHref(a.id)}"]) [data-slot="tab-title"]`)
|
||||
const title = page.locator(`[data-titlebar-tab-slot]:has(a[href="${sessionHref(b.id)}"]) [data-slot="tab-title"]`)
|
||||
const editor = page.locator('[data-slot="tab-title"][contenteditable="true"]')
|
||||
await expect(title).toHaveText(a.title)
|
||||
const save = await holdRoute(page, (url) => url.pathname === `/api/session/${a.id}`, { method: "PATCH" })
|
||||
await expect(title).toHaveText(b.title)
|
||||
const save = await holdRoute(page, (url) => url.pathname === `/api/session/${b.id}`, { method: "PATCH" })
|
||||
|
||||
await title.dblclick()
|
||||
await expect(editor).toBeFocused()
|
||||
@@ -189,6 +189,49 @@ test("option arrows move to the previous or next tab, and with shift to the next
|
||||
await expectPath(page, sessionHref(c.id))
|
||||
})
|
||||
|
||||
for (const navigation of ["horizontal", "vertical", "keyboard"] as const) {
|
||||
test(`switching session tabs restores the caret and pastes into the destination draft (${navigation})`, async ({
|
||||
page,
|
||||
context,
|
||||
}) => {
|
||||
const tabLayout = navigation === "vertical" ? "vertical" : "horizontal"
|
||||
|
||||
const strip = page.locator(
|
||||
tabLayout === "vertical" ? '[data-slot="vertical-tabs-sidebar"]' : '[data-slot="titlebar-tabs"]',
|
||||
)
|
||||
|
||||
await context.grantPermissions(["clipboard-read", "clipboard-write"])
|
||||
await openSession(page, {
|
||||
name: "TabFocus",
|
||||
sessions: [a, b],
|
||||
seed: { settings: { appearance: { tabLayout } } },
|
||||
})
|
||||
const editor = page.locator('[data-component="composer-editor"]')
|
||||
const tabB = strip.locator(`[data-titlebar-tab-link][href="${sessionHref(b.id)}"]`)
|
||||
await editor.fill("Draft A")
|
||||
|
||||
await (navigation === "keyboard" ? page.keyboard.press("Control+Alt+ArrowRight") : tabB.click())
|
||||
await expectPath(page, sessionHref(b.id))
|
||||
await expect(editor).toBeFocused()
|
||||
await page.evaluate(() => navigator.clipboard.writeText("Draft B"))
|
||||
await page.keyboard.press("ControlOrMeta+V")
|
||||
await expect(editor).toHaveText("Draft B")
|
||||
|
||||
await page.keyboard.press("Control+Alt+ArrowLeft")
|
||||
await expectPath(page, sessionHref(a.id))
|
||||
await expect(editor).toBeFocused()
|
||||
await expect(editor).toHaveText("Draft A")
|
||||
await page.evaluate(() => navigator.clipboard.writeText(" appended"))
|
||||
await page.keyboard.press("ControlOrMeta+V")
|
||||
await expect(editor).toHaveText("Draft A appended")
|
||||
|
||||
await tabB.click()
|
||||
await expectPath(page, sessionHref(b.id))
|
||||
await expect(editor).toBeFocused()
|
||||
await expect(editor).toHaveText("Draft B")
|
||||
})
|
||||
}
|
||||
|
||||
for (const row of [
|
||||
{ tabLayout: "horizontal", width: 360, hasTouch: false, direction: "ltr" },
|
||||
{ tabLayout: "vertical", width: 390, hasTouch: true, direction: "rtl" },
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { createEffect, createMemo, on, type Accessor } from "solid-js"
|
||||
import { createEffect, createMemo, on, onCleanup, type Accessor } from "solid-js"
|
||||
import { useDialog } from "@opencode/ui/context/dialog"
|
||||
import type { ComposerControls } from "@/composer/adapter"
|
||||
import { setCursorPosition } from "@/composer/editor/dom"
|
||||
import { promptLength } from "@/composer/prompt-parts"
|
||||
import { createComposerModel } from "@/composer/model"
|
||||
import { useSettings } from "@/settings/model"
|
||||
import { createActiveComposerAdapter } from "./adapter"
|
||||
@@ -14,6 +16,7 @@ export function createSessionComposerController(input: {
|
||||
dock: Parameters<typeof createSessionComposerRegionController>[0]
|
||||
}) {
|
||||
const settings = useSettings()
|
||||
const dialog = useDialog()
|
||||
const region = createSessionComposerRegionController(input.dock)
|
||||
let editor: HTMLDivElement | undefined
|
||||
|
||||
@@ -45,6 +48,26 @@ export function createSessionComposerController(input: {
|
||||
})
|
||||
|
||||
const composer = createComposerModel(adapter, { queue })
|
||||
// This controller is recreated when a session becomes active. Wait for its draft and
|
||||
// the editor's DOM commit, without reacting to subsequent cursor or message updates.
|
||||
createEffect(
|
||||
on(adapter.ready, (ready) => {
|
||||
if (!ready) return
|
||||
|
||||
const frame = requestAnimationFrame(() => {
|
||||
if (!editor?.isConnected || !region.showComposer() || region.child() || dialog.active) return
|
||||
const active = document.activeElement
|
||||
|
||||
// A control chosen while the session was loading owns focus now.
|
||||
if (active && active !== document.body && active !== editor && !active.matches("[data-titlebar-tab-link]"))
|
||||
return
|
||||
editor.focus({ preventScroll: true })
|
||||
setCursorPosition(editor, adapter.state.cursor() ?? promptLength(adapter.state.current()))
|
||||
})
|
||||
|
||||
onCleanup(() => cancelAnimationFrame(frame))
|
||||
}),
|
||||
)
|
||||
const editable = createMemo(() => region.showComposer() && !region.child())
|
||||
// Requests hide the view without disposing its draft or queue edit.
|
||||
createEffect(on(editable, () => composer.onDragLeave()))
|
||||
|
||||
Reference in New Issue
Block a user