From ffde837e8318900e92761ca8a7d9a0621143e04b Mon Sep 17 00:00:00 2001 From: David Hill Date: Thu, 12 Mar 2026 23:23:03 +0000 Subject: [PATCH 001/151] fix(app): animate titlebar controls on sidebar open --- packages/app/src/components/titlebar.tsx | 52 ++++++++++++++++-------- 1 file changed, 34 insertions(+), 18 deletions(-) diff --git a/packages/app/src/components/titlebar.tsx b/packages/app/src/components/titlebar.tsx index 3ab811e929..227670e427 100644 --- a/packages/app/src/components/titlebar.tsx +++ b/packages/app/src/components/titlebar.tsx @@ -217,26 +217,42 @@ export function Titlebar() { -
+
-
-
- -
- - } - > + + + + + { + const item = project() + if (!item) return + showEditProjectDialog(item) + }} + > + {language.t("common.edit")} + + { + const item = project() + if (!item) return + toggleProjectWorkspaces(item) + }} + > + + {workspacesEnabled() + ? language.t("sidebar.workspaces.disable") + : language.t("sidebar.workspaces.enable")} + + + + + {language.t("sidebar.project.clearNotifications")} + + + + { + const dir = worktree() + if (!dir) return + closeProject(dir) + }} + > + {language.t("common.close")} + + + + + + + +
+
-
-
- - - -
{ - if (!panelProps.mobile) scrollContainerRef = el - }} - class="size-full flex flex-col py-2 gap-4 overflow-y-auto no-scrollbar [overflow-anchor:none]" - > - - - {(directory) => ( - - )} - - -
- - store.activeWorkspace} - workspaceLabel={workspaceLabel} - /> - -
+
+
- -
- - )} + } + > + <> +
+ +
+
+ + + +
{ + if (!panelProps.mobile) scrollContainerRef = el + }} + class="size-full flex flex-col py-2 gap-4 overflow-y-auto no-scrollbar [overflow-anchor:none]" + > + + + {(directory) => ( + + )} + + +
+ + store.activeWorkspace} + workspaceLabel={workspaceLabel} + /> + +
+
+ +
+
+
platform.openLink("https://opencode.ai/desktop-feedback")} renderPanel={() => mobile ? ( - + ) : ( - + ) } @@ -2325,8 +2392,8 @@ export default function Layout(props: ParentProps) { arm() }} > - - + +
From 5c7088338c07ad632834ebd4a87feb23d255fb8a Mon Sep 17 00:00:00 2001 From: David Hill <1879069+iamdavidhill@users.noreply.github.com> Date: Fri, 13 Mar 2026 15:48:10 +0000 Subject: [PATCH 005/151] fix(app): polish prompt composer controls (#17388) --- packages/app/src/components/prompt-input.tsx | 123 +++++++++---------- packages/ui/src/components/icon.tsx | 1 + 2 files changed, 60 insertions(+), 64 deletions(-) diff --git a/packages/app/src/components/prompt-input.tsx b/packages/app/src/components/prompt-input.tsx index 3b78e80334..fd54de9a06 100644 --- a/packages/app/src/components/prompt-input.tsx +++ b/packages/app/src/components/prompt-input.tsx @@ -121,7 +121,7 @@ export const PromptInput: Component = (props) => { let slashPopoverRef!: HTMLDivElement const mirror = { input: false } - const inset = 52 + const inset = 56 const space = `${inset}px` const scrollCursorIntoView = () => { @@ -1031,6 +1031,17 @@ export const PromptInput: Component = (props) => { if (!id) return permission.isAutoAcceptingDirectory(sdk.directory) return permission.isAutoAccepting(id, sdk.directory) }) + const acceptLabel = createMemo(() => + language.t(accepting() ? "command.permissions.autoaccept.disable" : "command.permissions.autoaccept.enable"), + ) + const toggleAccept = () => { + if (!params.id) { + permission.toggleAutoAcceptDirectory(sdk.directory) + return + } + + permission.toggleAutoAccept(params.id, sdk.directory) + } const { abort, handleSubmit } = createPromptSubmit({ info, @@ -1337,33 +1348,7 @@ export const PromptInput: Component = (props) => { }} /> -
0.5 ? "auto" : "none", - }} - > - - - - +
= (props) => {
-
+
0.5 ? "auto" : "none", + }} + >
@@ -1468,8 +1441,8 @@ export const PromptInput: Component = (props) => { options={agentNames()} current={local.agent.current()?.name ?? ""} onSelect={local.agent.set} - class="capitalize max-w-[160px]" - valueClass="truncate text-13-regular" + class="capitalize max-w-[160px] text-text-base" + valueClass="truncate text-13-regular text-text-base" triggerStyle={control()} variant="ghost" /> @@ -1487,7 +1460,7 @@ export const PromptInput: Component = (props) => { as="div" variant="ghost" size="normal" - class="min-w-0 max-w-[320px] text-13-regular group" + class="min-w-0 max-w-[320px] text-13-regular text-text-base group" style={control()} onClick={() => dialog.show(() => )} > @@ -1518,7 +1491,7 @@ export const PromptInput: Component = (props) => { variant: "ghost", size: "normal", style: control(), - class: "min-w-0 max-w-[320px] text-13-regular group", + class: "min-w-0 max-w-[320px] text-13-regular text-text-base group", }} > @@ -1547,12 +1520,34 @@ export const PromptInput: Component = (props) => { current={local.model.variant.current() ?? "default"} label={(x) => (x === "default" ? language.t("common.default") : x)} onSelect={(x) => local.model.variant.set(x === "default" ? undefined : x)} - class="capitalize max-w-[160px]" - valueClass="truncate text-13-regular" + class="capitalize max-w-[160px] text-text-base" + valueClass="truncate text-13-regular text-text-base" triggerStyle={control()} variant="ghost" /> + + +
diff --git a/packages/ui/src/components/icon.tsx b/packages/ui/src/components/icon.tsx index aacfa476b5..e2eaf107a6 100644 --- a/packages/ui/src/components/icon.tsx +++ b/packages/ui/src/components/icon.tsx @@ -82,6 +82,7 @@ const icons = { check: ``, photo: ``, share: ``, + shield: ``, download: ``, menu: ``, server: ``, From 4ad8116ce37a0e77e7f3c0e9e4e1002bba05b15e Mon Sep 17 00:00:00 2001 From: Adam <2363879+adamdotdevin@users.noreply.github.com> Date: Fri, 13 Mar 2026 11:05:08 -0500 Subject: [PATCH 006/151] fix(app): model selection persist by session (#17348) --- packages/app/e2e/fixtures.ts | 3 + packages/app/e2e/selectors.ts | 3 + .../session/session-model-persistence.spec.ts | 351 +++++++++++ .../components/dialog-select-model-unpaid.tsx | 12 +- .../src/components/dialog-select-model.tsx | 19 +- packages/app/src/components/prompt-input.tsx | 155 ++--- .../components/prompt-input/submit.test.ts | 12 + .../app/src/components/prompt-input/submit.ts | 3 +- packages/app/src/context/local.tsx | 575 +++++++++++------- .../app/src/context/model-variant.test.ts | 20 + packages/app/src/context/model-variant.ts | 4 +- packages/app/src/pages/directory-layout.tsx | 6 +- packages/app/src/pages/session.tsx | 4 +- .../session/session-model-helpers.test.ts | 133 +--- .../pages/session/session-model-helpers.ts | 42 +- .../pages/session/use-session-commands.tsx | 2 +- packages/app/src/testing/model-selection.ts | 80 +++ packages/app/src/testing/terminal.ts | 6 + packages/ui/src/components/select.tsx | 3 + 19 files changed, 981 insertions(+), 452 deletions(-) create mode 100644 packages/app/e2e/session/session-model-persistence.spec.ts create mode 100644 packages/app/src/testing/model-selection.ts diff --git a/packages/app/e2e/fixtures.ts b/packages/app/e2e/fixtures.ts index cf59eeb476..efefd479ef 100644 --- a/packages/app/e2e/fixtures.ts +++ b/packages/app/e2e/fixtures.ts @@ -95,6 +95,9 @@ async function seedStorage(page: Page, input: { directory: string; extra?: strin const win = window as E2EWindow win.__opencode_e2e = { ...win.__opencode_e2e, + model: { + enabled: true, + }, terminal: { enabled: true, terminals: {}, diff --git a/packages/app/e2e/selectors.ts b/packages/app/e2e/selectors.ts index 64b7bfe545..80b6c473d6 100644 --- a/packages/app/e2e/selectors.ts +++ b/packages/app/e2e/selectors.ts @@ -13,6 +13,9 @@ export const sessionTodoToggleButtonSelector = '[data-action="session-todo-toggl export const sessionTodoListSelector = '[data-slot="session-todo-list"]' export const modelVariantCycleSelector = '[data-action="model-variant-cycle"]' +export const promptAgentSelector = '[data-component="prompt-agent-control"]' +export const promptModelSelector = '[data-component="prompt-model-control"]' +export const promptVariantSelector = '[data-component="prompt-variant-control"]' export const settingsLanguageSelectSelector = '[data-action="settings-language"]' export const settingsColorSchemeSelector = '[data-action="settings-color-scheme"]' export const settingsThemeSelector = '[data-action="settings-theme"]' diff --git a/packages/app/e2e/session/session-model-persistence.spec.ts b/packages/app/e2e/session/session-model-persistence.spec.ts new file mode 100644 index 0000000000..4b09a52873 --- /dev/null +++ b/packages/app/e2e/session/session-model-persistence.spec.ts @@ -0,0 +1,351 @@ +import { base64Decode } from "@opencode-ai/util/encode" +import type { Locator, Page } from "@playwright/test" +import { test, expect } from "../fixtures" +import { openSidebar, sessionIDFromUrl, setWorkspacesEnabled, waitSessionIdle, waitSlug } from "../actions" +import { + promptAgentSelector, + promptModelSelector, + promptSelector, + promptVariantSelector, + workspaceItemSelector, + workspaceNewSessionSelector, +} from "../selectors" +import { createSdk, sessionPath } from "../utils" + +type Footer = { + agent: string + model: string + variant: string +} + +type Probe = { + dir?: string + sessionID?: string + model?: { providerID: string; modelID: string } +} + +const escape = (value: string) => value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&") + +const text = async (locator: Locator) => ((await locator.textContent()) ?? "").trim() + +const modelKey = (state: Probe | null) => (state?.model ? `${state.model.providerID}:${state.model.modelID}` : null) + +const dirKey = (state: Probe | null) => state?.dir ?? "" + +async function probe(page: Page): Promise { + return page.evaluate(() => { + const win = window as Window & { + __opencode_e2e?: { + model?: { + current?: Probe + } + } + } + return win.__opencode_e2e?.model?.current ?? null + }) +} + +async function currentDir(page: Page) { + let hit = "" + await expect + .poll( + async () => { + const next = dirKey(await probe(page)) + if (next) hit = next + return next + }, + { timeout: 30_000 }, + ) + .not.toBe("") + return hit +} + +async function read(page: Page): Promise