From cdce4d9caa44d403ac163a2d5e77f12c6a917d33 Mon Sep 17 00:00:00 2001 From: Sean Tichenor Date: Tue, 10 Mar 2026 13:28:48 -0500 Subject: [PATCH 1/2] fix: keep Copilot Claude prompts user-final --- packages/opencode/src/provider/transform.ts | 37 ++++ packages/opencode/src/session/prompt.ts | 89 ++++++++- .../convert-to-copilot-messages.test.ts | 64 ++++++ .../opencode/test/provider/transform.test.ts | 149 +++++++++++++- packages/opencode/test/session/prompt.test.ts | 182 ++++++++++++++++++ 5 files changed, 516 insertions(+), 5 deletions(-) diff --git a/packages/opencode/src/provider/transform.ts b/packages/opencode/src/provider/transform.ts index 407c2687825..b159c6cbaaa 100644 --- a/packages/opencode/src/provider/transform.ts +++ b/packages/opencode/src/provider/transform.ts @@ -17,9 +17,23 @@ function mimeToModality(mime: string): Modality | undefined { return undefined } +const USER_FINAL_TURN_SAFETY_NET_REMINDER = [ + "", + "Continue from the conversation above.", + "Follow the most recent assistant instructions and tool results.", + "Do not repeat completed work.", + "", +].join("\n") + export namespace ProviderTransform { export const OUTPUT_TOKEN_MAX = Flag.OPENCODE_EXPERIMENTAL_OUTPUT_TOKEN_MAX || 32_000 + export function requiresUserFinalTurn(model: Provider.Model): boolean { + if (model.api.npm === "@ai-sdk/anthropic" || model.api.npm === "@ai-sdk/google-vertex/anthropic") return true + if (model.api.npm === "@ai-sdk/github-copilot" && model.id.toLowerCase().includes("claude")) return true + return false + } + // Maps npm package to the key the AI SDK expects for providerOptions function sdkKey(npm: string): string | undefined { switch (npm) { @@ -171,6 +185,26 @@ export namespace ProviderTransform { return msgs } + function ensureUserFinalTurn(msgs: ModelMessage[], model: Provider.Model): ModelMessage[] { + if (!requiresUserFinalTurn(model)) return msgs + + const last = msgs.at(-1) + if (!last || last.role === "user") return msgs + + return [ + ...msgs, + { + role: "user", + content: [ + { + type: "text", + text: USER_FINAL_TURN_SAFETY_NET_REMINDER, + }, + ], + }, + ] + } + function applyCaching(msgs: ModelMessage[], model: Provider.Model): ModelMessage[] { const system = msgs.filter((msg) => msg.role === "system").slice(0, 2) const final = msgs.filter((msg) => msg.role !== "system").slice(-2) @@ -252,6 +286,9 @@ export namespace ProviderTransform { export function message(msgs: ModelMessage[], model: Provider.Model, options: Record) { msgs = unsupportedParts(msgs, model) msgs = normalizeMessages(msgs, model, options) + // The session loop should normally keep Anthropic-style providers user-final, + // but repair the prompt again here as a last-line transport guard. + msgs = ensureUserFinalTurn(msgs, model) if ( (model.providerID === "anthropic" || model.api.id.includes("anthropic") || diff --git a/packages/opencode/src/session/prompt.ts b/packages/opencode/src/session/prompt.ts index 7698b78baba..5cd40799ae2 100644 --- a/packages/opencode/src/session/prompt.ts +++ b/packages/opencode/src/session/prompt.ts @@ -59,6 +59,17 @@ IMPORTANT: - This tool provides your final answer - no further actions are taken after calling it` const STRUCTURED_OUTPUT_SYSTEM_PROMPT = `IMPORTANT: The user has requested structured output. You MUST use the StructuredOutput tool to provide your final response. Do NOT respond with plain text - you MUST call the StructuredOutput tool with your answer formatted according to the schema.` +const CONTINUE_TASK_SYSTEM_REMINDER = [ + "", + "Continue with your task using the conversation and tool results above.", + "Do not repeat completed work.", + "", +].join("\n") +const MAX_STEPS_SYSTEM_REMINDER = [ + "", + MAX_STEPS, + "", +].join("\n") export namespace SessionPrompt { const log = Log.create({ service: "session.prompt" }) @@ -647,6 +658,11 @@ export namespace SessionPrompt { } } + msgs = appendSyntheticContinuationTurn(msgs, lastUser, model, { + forceFinalUser: isLastStep, + text: isLastStep ? MAX_STEPS_SYSTEM_REMINDER : CONTINUE_TASK_SYSTEM_REMINDER, + }) + await Plugin.trigger("experimental.chat.messages.transform", {}, { messages: msgs }) // Build system prompt, adding structured output instruction if needed @@ -664,7 +680,7 @@ export namespace SessionPrompt { system, messages: [ ...MessageV2.toModelMessages(msgs, model), - ...(isLastStep + ...(isLastStep && !ProviderTransform.requiresUserFinalTurn(model) ? [ { role: "assistant" as const, @@ -1959,4 +1975,75 @@ NOTE: At any point in time through this workflow you should feel free to ask the return Session.setTitle({ sessionID: input.session.id, title }) } } + + /** @internal Exported for testing — appends an ephemeral user turn for Anthropic-style providers when looping */ + export function appendSyntheticContinuationTurn( + messages: MessageV2.WithParts[], + lastUser: MessageV2.User, + model: Provider.Model, + options: { + text?: string + forceFinalUser?: boolean + } = {}, + ): MessageV2.WithParts[] { + if (!ProviderTransform.requiresUserFinalTurn(model)) return messages + + const lastMessage = messages.at(-1) + if (!lastMessage) return messages + + const text = options.text ?? CONTINUE_TASK_SYSTEM_REMINDER + if (lastMessage.info.role === "user") { + if (!options.forceFinalUser) return messages + + // When the first model call is also the last allowed step, keep the prompt + // user-final by ephemerally appending the reminder to the trailing user turn. + return [ + ...messages.slice(0, -1), + { + ...lastMessage, + parts: [ + ...lastMessage.parts, + { + id: Identifier.ascending("part"), + sessionID: lastMessage.info.sessionID, + messageID: lastMessage.info.id, + type: "text", + text, + synthetic: true, + }, + ], + }, + ] + } + + if (lastMessage.info.role !== "assistant") return messages + + const messageID = Identifier.ascending("message") + return [ + ...messages, + { + info: { + id: messageID, + sessionID: lastUser.sessionID, + role: "user", + time: { + created: Date.now(), + }, + agent: lastUser.agent, + model: lastUser.model, + variant: lastUser.variant, + }, + parts: [ + { + id: Identifier.ascending("part"), + sessionID: lastUser.sessionID, + messageID, + type: "text", + text, + synthetic: true, + }, + ], + }, + ] + } } diff --git a/packages/opencode/test/provider/copilot/convert-to-copilot-messages.test.ts b/packages/opencode/test/provider/copilot/convert-to-copilot-messages.test.ts index 6f874db6d2e..8db6ec639d1 100644 --- a/packages/opencode/test/provider/copilot/convert-to-copilot-messages.test.ts +++ b/packages/opencode/test/provider/copilot/convert-to-copilot-messages.test.ts @@ -1,4 +1,5 @@ import { convertToOpenAICompatibleChatMessages as convertToCopilotMessages } from "@/provider/sdk/copilot/chat/convert-to-openai-compatible-chat-messages" +import { ProviderTransform } from "@/provider/transform" import { describe, test, expect } from "bun:test" describe("system messages", () => { @@ -521,3 +522,66 @@ describe("full conversation", () => { expect(assistantMsg.reasoning_opaque).toBe("sig-abc") }) }) + +describe("final user turn guard", () => { + test("serializes a user-final Copilot Claude prompt after provider repair", () => { + const model = { + id: "claude-opus-4.6", + providerID: "github-copilot", + api: { + id: "claude-opus-4.6", + url: "https://api.githubcopilot.com", + npm: "@ai-sdk/github-copilot", + }, + name: "Claude Opus 4.6", + capabilities: { + temperature: true, + reasoning: true, + attachment: true, + toolcall: true, + input: { text: true, audio: false, image: true, video: false, pdf: true }, + output: { text: true, audio: false, image: false, video: false, pdf: false }, + interleaved: false, + }, + cost: { + input: 0.003, + output: 0.015, + cache: { read: 0.0003, write: 0.00375 }, + }, + limit: { + context: 200000, + output: 8192, + }, + status: "active", + options: {}, + headers: {}, + } as any + + const repaired = ProviderTransform.message( + [ + { + role: "assistant", + content: [{ type: "text", text: "Let me keep going." }], + }, + ] as any[], + model, + {}, + ) + const result = convertToCopilotMessages(repaired as any) + + expect(result).toHaveLength(2) + expect(result[0]).toEqual({ + role: "assistant", + content: "Let me keep going.", + tool_calls: undefined, + reasoning_text: undefined, + reasoning_opaque: undefined, + }) + expect(result[1]).toEqual( + expect.objectContaining({ + role: "user", + content: expect.stringContaining("Continue from the conversation above."), + }), + ) + }) +}) diff --git a/packages/opencode/test/provider/transform.test.ts b/packages/opencode/test/provider/transform.test.ts index 9dde1a7131e..68388630884 100644 --- a/packages/opencode/test/provider/transform.test.ts +++ b/packages/opencode/test/provider/transform.test.ts @@ -1008,9 +1008,10 @@ describe("ProviderTransform.message - anthropic empty content filtering", () => const result = ProviderTransform.message(msgs, anthropicModel, {}) - expect(result).toHaveLength(1) + expect(result).toHaveLength(2) expect(result[0].content).toHaveLength(1) expect(result[0].content[0]).toEqual({ type: "text", text: "Hello" }) + expect(result[1].role).toBe("user") }) test("filters out empty reasoning parts from array content", () => { @@ -1027,9 +1028,10 @@ describe("ProviderTransform.message - anthropic empty content filtering", () => const result = ProviderTransform.message(msgs, anthropicModel, {}) - expect(result).toHaveLength(1) + expect(result).toHaveLength(2) expect(result[0].content).toHaveLength(1) expect(result[0].content[0]).toEqual({ type: "text", text: "Answer" }) + expect(result[1].role).toBe("user") }) test("removes entire message when all parts are empty", () => { @@ -1065,7 +1067,7 @@ describe("ProviderTransform.message - anthropic empty content filtering", () => const result = ProviderTransform.message(msgs, anthropicModel, {}) - expect(result).toHaveLength(1) + expect(result).toHaveLength(2) expect(result[0].content).toHaveLength(1) expect(result[0].content[0]).toEqual({ type: "tool-call", @@ -1073,6 +1075,7 @@ describe("ProviderTransform.message - anthropic empty content filtering", () => toolName: "bash", input: { command: "ls" }, }) + expect(result[1].role).toBe("user") }) test("keeps messages with valid text alongside empty parts", () => { @@ -1089,10 +1092,11 @@ describe("ProviderTransform.message - anthropic empty content filtering", () => const result = ProviderTransform.message(msgs, anthropicModel, {}) - expect(result).toHaveLength(1) + expect(result).toHaveLength(2) expect(result[0].content).toHaveLength(2) expect(result[0].content[0]).toEqual({ type: "reasoning", text: "Thinking..." }) expect(result[0].content[1]).toEqual({ type: "text", text: "Result" }) + expect(result[1].role).toBe("user") }) test("does not filter for non-anthropic providers", () => { @@ -1413,6 +1417,143 @@ describe("ProviderTransform.message - strip openai metadata when store=false", ( }) }) +describe("ProviderTransform.message - user-final provider guard", () => { + const createClaudeModel = (overrides: Partial = {}) => + ({ + id: "claude-opus-4.6", + providerID: "github-copilot", + api: { + id: "claude-opus-4.6", + url: "https://api.githubcopilot.com", + npm: "@ai-sdk/github-copilot", + }, + name: "Claude Opus 4.6", + capabilities: { + temperature: true, + reasoning: true, + attachment: true, + toolcall: true, + input: { text: true, audio: false, image: true, video: false, pdf: true }, + output: { text: true, audio: false, image: false, video: false, pdf: false }, + interleaved: false, + }, + cost: { + input: 0.003, + output: 0.015, + cache: { read: 0.0003, write: 0.00375 }, + }, + limit: { + context: 200000, + output: 8192, + }, + status: "active", + options: {}, + headers: {}, + ...overrides, + }) as any + + test("appends a synthetic user reminder for Copilot Claude assistant-final prompts", () => { + const model = createClaudeModel() + const msgs = [ + { + role: "assistant", + content: [{ type: "text", text: "Let me check that for you." }], + }, + ] as any[] + + const result = ProviderTransform.message(msgs, model, {}) as any[] + + expect(result).toHaveLength(2) + expect(result[0].role).toBe("assistant") + expect(result[1]).toEqual( + expect.objectContaining({ + role: "user", + content: [ + expect.objectContaining({ + type: "text", + text: expect.stringContaining("Follow the most recent assistant instructions and tool results."), + }), + ], + }), + ) + }) + + test("appends a synthetic user reminder when an Anthropic-style prompt ends with a tool result", () => { + const model = createClaudeModel({ + providerID: "anthropic", + api: { + id: "claude-sonnet-4", + url: "https://api.anthropic.com", + npm: "@ai-sdk/anthropic", + }, + }) + const msgs = [ + { + role: "assistant", + content: [ + { + type: "tool-call", + toolCallId: "call_1", + toolName: "bash", + input: { command: "ls" }, + }, + ], + }, + { + role: "tool", + content: [ + { + type: "tool-result", + toolCallId: "call_1", + toolName: "bash", + output: { type: "text", value: "file.txt" }, + }, + ], + }, + ] as any[] + + const result = ProviderTransform.message(msgs, model, {}) as any[] + + expect(result).toHaveLength(3) + expect(result[2]).toEqual( + expect.objectContaining({ + role: "user", + content: [ + expect.objectContaining({ + type: "text", + text: expect.stringContaining("Continue from the conversation above."), + }), + ], + }), + ) + }) + + test("does not alter assistant-final prompts for providers that allow them", () => { + const model = createClaudeModel({ + id: "openai/gpt-5.2", + providerID: "openai", + api: { + id: "gpt-5.2", + url: "https://api.openai.com", + npm: "@ai-sdk/openai", + }, + name: "GPT-5.2", + }) + const msgs = [ + { + role: "assistant", + content: [{ type: "text", text: "Done." }], + }, + ] as any[] + + const result = ProviderTransform.message(msgs, model, {}) as any[] + + expect(result).toHaveLength(1) + expect(result[0].role).toBe("assistant") + expect(result[0].content).toEqual([{ type: "text", text: "Done." }]) + }) +}) + describe("ProviderTransform.message - providerOptions key remapping", () => { const createModel = (providerID: string, npm: string) => ({ diff --git a/packages/opencode/test/session/prompt.test.ts b/packages/opencode/test/session/prompt.test.ts index e8a8c65b03d..fcace906e88 100644 --- a/packages/opencode/test/session/prompt.test.ts +++ b/packages/opencode/test/session/prompt.test.ts @@ -209,3 +209,185 @@ describe("session.prompt agent variant", () => { } }) }) + +describe("session.prompt continuation turns", () => { + test("appends a synthetic user turn for Copilot Claude when the loop would continue after an assistant turn", () => { + const userInfo: MessageV2.User = { + id: "msg_user", + sessionID: "ses_test", + role: "user", + time: { created: 1 }, + agent: "build", + model: { providerID: "github-copilot", modelID: "claude-opus-4.6" }, + } + const assistantInfo: MessageV2.Assistant = { + id: "msg_assistant", + sessionID: "ses_test", + role: "assistant", + time: { created: 2, completed: 3 }, + parentID: "msg_user", + modelID: "claude-opus-4.6", + providerID: "github-copilot", + mode: "build", + agent: "build", + path: { cwd: "/tmp", root: "/tmp" }, + cost: 0, + tokens: { + input: 0, + output: 0, + reasoning: 0, + cache: { read: 0, write: 0 }, + }, + finish: "tool-calls", + } + const messages: MessageV2.WithParts[] = [ + { + info: userInfo, + parts: [ + { + id: "prt_user", + messageID: "msg_user", + sessionID: "ses_test", + type: "text", + text: "Continue.", + }, + ], + }, + { + info: assistantInfo, + parts: [ + { + id: "prt_assistant", + messageID: "msg_assistant", + sessionID: "ses_test", + type: "text", + text: "It's running. Let me check again.", + }, + ], + }, + ] + + const result = SessionPrompt.appendSyntheticContinuationTurn(messages, userInfo, { + id: "claude-opus-4.6", + api: { npm: "@ai-sdk/github-copilot" }, + } as any) + + expect(result).toHaveLength(3) + expect(result[2].info.role).toBe("user") + expect(result[2].parts).toHaveLength(1) + expect(result[2].parts[0].type).toBe("text") + expect("synthetic" in result[2].parts[0] && result[2].parts[0].synthetic).toBe(true) + expect(result[2].parts[0].type === "text" && result[2].parts[0].text).toContain( + "Continue with your task using the conversation and tool results above.", + ) + }) + + test("appends the max-step reminder to the trailing user turn when forcing a user-final prompt", () => { + const userInfo: MessageV2.User = { + id: "msg_user", + sessionID: "ses_test", + role: "user", + time: { created: 1 }, + agent: "build", + model: { providerID: "github-copilot", modelID: "claude-opus-4.6" }, + } + const messages: MessageV2.WithParts[] = [ + { + info: userInfo, + parts: [ + { + id: "prt_user", + messageID: "msg_user", + sessionID: "ses_test", + type: "text", + text: "Continue.", + }, + ], + }, + ] + + const result = SessionPrompt.appendSyntheticContinuationTurn( + messages, + userInfo, + { + id: "claude-opus-4.6", + api: { npm: "@ai-sdk/github-copilot" }, + } as any, + { + forceFinalUser: true, + text: "\nCRITICAL - MAXIMUM STEPS REACHED\n", + }, + ) + + expect(result).toHaveLength(1) + expect(result[0].info.role).toBe("user") + expect(result[0].parts).toHaveLength(2) + expect(result[0].parts[1].type).toBe("text") + expect("synthetic" in result[0].parts[1] && result[0].parts[1].synthetic).toBe(true) + expect(result[0].parts[1].type === "text" && result[0].parts[1].text).toContain("MAXIMUM STEPS REACHED") + }) + + test("does not append a synthetic user turn for providers that allow assistant-final turns", () => { + const userInfo: MessageV2.User = { + id: "msg_user", + sessionID: "ses_test", + role: "user", + time: { created: 1 }, + agent: "build", + model: { providerID: "openai", modelID: "gpt-5.2" }, + } + const assistantInfo: MessageV2.Assistant = { + id: "msg_assistant", + sessionID: "ses_test", + role: "assistant", + time: { created: 2, completed: 3 }, + parentID: "msg_user", + modelID: "gpt-5.2", + providerID: "openai", + mode: "build", + agent: "build", + path: { cwd: "/tmp", root: "/tmp" }, + cost: 0, + tokens: { + input: 0, + output: 0, + reasoning: 0, + cache: { read: 0, write: 0 }, + }, + finish: "tool-calls", + } + const messages: MessageV2.WithParts[] = [ + { + info: userInfo, + parts: [ + { + id: "prt_user", + messageID: "msg_user", + sessionID: "ses_test", + type: "text", + text: "Continue.", + }, + ], + }, + { + info: assistantInfo, + parts: [ + { + id: "prt_assistant", + messageID: "msg_assistant", + sessionID: "ses_test", + type: "text", + text: "Working on it.", + }, + ], + }, + ] + + const result = SessionPrompt.appendSyntheticContinuationTurn(messages, userInfo, { + id: "gpt-5.2", + api: { npm: "@ai-sdk/openai" }, + } as any) + + expect(result).toStrictEqual(messages) + }) +}) From a8eeb068de505d69b82fe08be332d8bd98139326 Mon Sep 17 00:00:00 2001 From: Sean Tichenor Date: Tue, 10 Mar 2026 14:06:40 -0500 Subject: [PATCH 2/2] test(app): use terminal panel selector for visibility checks --- packages/app/e2e/prompt/prompt-slash-terminal.spec.ts | 8 +++++--- packages/app/e2e/selectors.ts | 1 + packages/app/e2e/settings/settings-keybinds.spec.ts | 8 +++++--- packages/app/e2e/terminal/terminal-init.spec.ts | 6 ++++-- packages/app/e2e/terminal/terminal-tabs.spec.ts | 6 ++++-- packages/app/e2e/terminal/terminal.spec.ts | 8 +++++--- 6 files changed, 24 insertions(+), 13 deletions(-) diff --git a/packages/app/e2e/prompt/prompt-slash-terminal.spec.ts b/packages/app/e2e/prompt/prompt-slash-terminal.spec.ts index eefce19dc0b..d44868e51fd 100644 --- a/packages/app/e2e/prompt/prompt-slash-terminal.spec.ts +++ b/packages/app/e2e/prompt/prompt-slash-terminal.spec.ts @@ -1,23 +1,25 @@ import { test, expect } from "../fixtures" -import { promptSelector, terminalSelector } from "../selectors" +import { promptSelector, terminalPanelSelector, terminalSelector } from "../selectors" test("/terminal toggles the terminal panel", async ({ page, gotoSession }) => { await gotoSession() const prompt = page.locator(promptSelector) + const terminalPanel = page.locator(terminalPanelSelector) const terminal = page.locator(terminalSelector) - await expect(terminal).not.toBeVisible() + await expect(terminalPanel).toHaveCount(0) await prompt.click() await page.keyboard.type("/terminal") await expect(page.locator('[data-slash-id="terminal.toggle"]').first()).toBeVisible() await page.keyboard.press("Enter") + await expect(terminalPanel).toBeVisible() await expect(terminal).toBeVisible() await prompt.click() await page.keyboard.type("/terminal") await expect(page.locator('[data-slash-id="terminal.toggle"]').first()).toBeVisible() await page.keyboard.press("Enter") - await expect(terminal).not.toBeVisible() + await expect(terminalPanel).toHaveCount(0) }) diff --git a/packages/app/e2e/selectors.ts b/packages/app/e2e/selectors.ts index 2061a112849..eec02a4c968 100644 --- a/packages/app/e2e/selectors.ts +++ b/packages/app/e2e/selectors.ts @@ -1,4 +1,5 @@ export const promptSelector = '[data-component="prompt-input"]' +export const terminalPanelSelector = "#terminal-panel" export const terminalSelector = '[data-component="terminal"]' export const sessionComposerDockSelector = '[data-component="session-prompt-dock"]' export const questionDockSelector = '[data-component="dock-prompt"][data-kind="question"]' diff --git a/packages/app/e2e/settings/settings-keybinds.spec.ts b/packages/app/e2e/settings/settings-keybinds.spec.ts index e0d590b31af..c2b0c072a21 100644 --- a/packages/app/e2e/settings/settings-keybinds.spec.ts +++ b/packages/app/e2e/settings/settings-keybinds.spec.ts @@ -1,6 +1,6 @@ import { test, expect } from "../fixtures" import { openSettings, closeDialog, withSession } from "../actions" -import { keybindButtonSelector, terminalSelector } from "../selectors" +import { keybindButtonSelector, terminalPanelSelector, terminalSelector } from "../selectors" import { modKey } from "../utils" test("changing sidebar toggle keybind works", async ({ page, gotoSession }) => { @@ -298,14 +298,16 @@ test("changing terminal toggle keybind works", async ({ page, gotoSession }) => await closeDialog(page, dialog) + const terminalPanel = page.locator(terminalPanelSelector) const terminal = page.locator(terminalSelector) - await expect(terminal).not.toBeVisible() + await expect(terminalPanel).toHaveCount(0) await page.keyboard.press(`${modKey}+Y`) + await expect(terminalPanel).toBeVisible() await expect(terminal).toBeVisible() await page.keyboard.press(`${modKey}+Y`) - await expect(terminal).not.toBeVisible() + await expect(terminalPanel).toHaveCount(0) }) test("terminal toggle keybind persists after reload", async ({ page, gotoSession }) => { diff --git a/packages/app/e2e/terminal/terminal-init.spec.ts b/packages/app/e2e/terminal/terminal-init.spec.ts index 18991bf7636..74b3870a33e 100644 --- a/packages/app/e2e/terminal/terminal-init.spec.ts +++ b/packages/app/e2e/terminal/terminal-init.spec.ts @@ -1,18 +1,20 @@ import { test, expect } from "../fixtures" -import { promptSelector, terminalSelector } from "../selectors" +import { promptSelector, terminalPanelSelector, terminalSelector } from "../selectors" import { terminalToggleKey } from "../utils" test("smoke terminal mounts and can create a second tab", async ({ page, gotoSession }) => { await gotoSession() + const terminalPanel = page.locator(terminalPanelSelector) const terminals = page.locator(terminalSelector) const tabs = page.locator('#terminal-panel [data-slot="tabs-trigger"]') - const opened = await terminals.first().isVisible() + const opened = await terminalPanel.isVisible().catch(() => false) if (!opened) { await page.keyboard.press(terminalToggleKey) } + await expect(terminalPanel).toBeVisible() await expect(terminals.first()).toBeVisible() await expect(terminals.first().locator("textarea")).toHaveCount(1) await expect(terminals).toHaveCount(1) diff --git a/packages/app/e2e/terminal/terminal-tabs.spec.ts b/packages/app/e2e/terminal/terminal-tabs.spec.ts index afa6254cd0a..5c021dfeed9 100644 --- a/packages/app/e2e/terminal/terminal-tabs.spec.ts +++ b/packages/app/e2e/terminal/terminal-tabs.spec.ts @@ -1,6 +1,6 @@ import type { Page } from "@playwright/test" import { test, expect } from "../fixtures" -import { terminalSelector } from "../selectors" +import { terminalPanelSelector, terminalSelector } from "../selectors" import { terminalToggleKey, workspacePersistKey } from "../utils" type State = { @@ -14,9 +14,11 @@ type State = { } async function open(page: Page) { + const terminalPanel = page.locator(terminalPanelSelector) const terminal = page.locator(terminalSelector) - const visible = await terminal.isVisible().catch(() => false) + const visible = await terminalPanel.isVisible().catch(() => false) if (!visible) await page.keyboard.press(terminalToggleKey) + await expect(terminalPanel).toBeVisible() await expect(terminal).toBeVisible() await expect(terminal.locator("textarea")).toHaveCount(1) } diff --git a/packages/app/e2e/terminal/terminal.spec.ts b/packages/app/e2e/terminal/terminal.spec.ts index ef88aa34e52..15f82a7b409 100644 --- a/packages/app/e2e/terminal/terminal.spec.ts +++ b/packages/app/e2e/terminal/terminal.spec.ts @@ -1,17 +1,19 @@ import { test, expect } from "../fixtures" -import { terminalSelector } from "../selectors" +import { terminalPanelSelector, terminalSelector } from "../selectors" import { terminalToggleKey } from "../utils" test("terminal panel can be toggled", async ({ page, gotoSession }) => { await gotoSession() + const terminalPanel = page.locator(terminalPanelSelector) const terminal = page.locator(terminalSelector) - const initiallyOpen = await terminal.isVisible() + const initiallyOpen = await terminalPanel.isVisible().catch(() => false) if (initiallyOpen) { await page.keyboard.press(terminalToggleKey) - await expect(terminal).toHaveCount(0) + await expect(terminalPanel).toHaveCount(0) } await page.keyboard.press(terminalToggleKey) + await expect(terminalPanel).toBeVisible() await expect(terminal).toBeVisible() })