diff --git a/bun.lock b/bun.lock index 2a6a28b7d4..8db8852a0b 100644 --- a/bun.lock +++ b/bun.lock @@ -358,7 +358,7 @@ "drizzle-orm": "catalog:", "effect": "catalog:", "fuzzysort": "3.1.0", - "gitlab-ai-provider": "5.2.2", + "gitlab-ai-provider": "5.3.1", "glob": "13.0.5", "google-auth-library": "10.5.0", "gray-matter": "4.0.3", @@ -3036,7 +3036,7 @@ "github-slugger": ["github-slugger@2.0.0", "", {}, "sha512-IaOQ9puYtjrkq7Y0Ygl9KDZnrf/aiUJYUpVf89y8kyaxbRG7Y1SrX/jaumrv81vc61+kiMempujsM3Yw7w5qcw=="], - "gitlab-ai-provider": ["gitlab-ai-provider@5.2.2", "", { "dependencies": { "@anthropic-ai/sdk": "^0.71.0", "@anycable/core": "^0.9.2", "graphql-request": "^6.1.0", "isomorphic-ws": "^5.0.0", "openai": "^6.16.0", "socket.io-client": "^4.8.1", "vscode-jsonrpc": "^8.2.1", "zod": "^3.25.76" }, "peerDependencies": { "@ai-sdk/provider": ">=2.0.0", "@ai-sdk/provider-utils": ">=3.0.0" } }, "sha512-ejwnie62rimfVHbjYZ2tsnqwLjF9YLgXD3OQA458gHz8hUvw7vEnhuyuMv5PmWQtyS3ISAghiX7r5SBhUWeCTA=="], + "gitlab-ai-provider": ["gitlab-ai-provider@5.3.1", "", { "dependencies": { "@anthropic-ai/sdk": "^0.71.0", "@anycable/core": "^0.9.2", "graphql-request": "^6.1.0", "isomorphic-ws": "^5.0.0", "openai": "^6.16.0", "socket.io-client": "^4.8.1", "vscode-jsonrpc": "^8.2.1", "zod": "^3.25.76" }, "peerDependencies": { "@ai-sdk/provider": ">=2.0.0", "@ai-sdk/provider-utils": ">=3.0.0" } }, "sha512-QeNP2af/5wyOHYaLvDxn72n4xbMbJNqRiKExZJM8MnynebnqnoaJoojbtue7roCl/XcnjX6Of2+oc7hS44S45Q=="], "glob": ["glob@13.0.5", "", { "dependencies": { "minimatch": "^10.2.1", "minipass": "^7.1.2", "path-scurry": "^2.0.0" } }, "sha512-BzXxZg24Ibra1pbQ/zE7Kys4Ua1ks7Bn6pKLkVPZ9FZe4JQS6/Q7ef3LG1H+k7lUf5l4T3PLSyYyYJVYUvfgTw=="], diff --git a/nix/hashes.json b/nix/hashes.json index a965ed58bd..2065007431 100644 --- a/nix/hashes.json +++ b/nix/hashes.json @@ -1,8 +1,8 @@ { "nodeModules": { - "x86_64-linux": "sha256-E5neEbBiwQDhIQ5QVhijpHCCP9hcxm319S9WrDKngSw=", - "aarch64-linux": "sha256-lnwaGSEirl9izskDooB/xQ0ZdirW0t3/S+OoOnfYaoQ=", - "aarch64-darwin": "sha256-RDxxW9NMlGMIdIxTsbOYVqxunflkILv2dA7JqjnJgm4=", - "x86_64-darwin": "sha256-1tvvktu2NRg6N6ASuKzqzcEmMrzH3/LFey0Vxr4E8zg=" + "x86_64-linux": "sha256-nMERinypUtIZGfLlAS5meYrvH5tTl2SkdG3GUguhOos=", + "aarch64-linux": "sha256-aQ42YVcjXSxpweA3e0SfJ8mnMWEqGeIOKg1cIhn8szA=", + "aarch64-darwin": "sha256-OGtUfhKWTRqi8bYcqkvfb1RZa3iS0DVy5bbRry47Og4=", + "x86_64-darwin": "sha256-kdzsr67cGduvGl+4UVdngiKNCaVw88WeMgx1ckVbG30=" } } diff --git a/packages/app/src/pages/session.tsx b/packages/app/src/pages/session.tsx index 428826f6ad..19dcba58ee 100644 --- a/packages/app/src/pages/session.tsx +++ b/packages/app/src/pages/session.tsx @@ -41,7 +41,13 @@ import { useSync } from "@/context/sync" import { useTerminal } from "@/context/terminal" import { type FollowupDraft, sendFollowupDraft } from "@/components/prompt-input/submit" import { createSessionComposerState, SessionComposerRegion } from "@/pages/session/composer" -import { createOpenReviewFile, createSessionTabs, createSizing, focusTerminalById } from "@/pages/session/helpers" +import { + createOpenReviewFile, + createSessionTabs, + createSizing, + focusTerminalById, + shouldFocusTerminalOnKeyDown, +} from "@/pages/session/helpers" import { MessageTimeline } from "@/pages/session/message-timeline" import { type DiffStyle, SessionReviewTab, type SessionReviewTabProps } from "@/pages/session/review-tab" import { useSessionLayout } from "@/pages/session/session-layout" @@ -850,7 +856,7 @@ export default function Page() { // Prefer the open terminal over the composer when it can take focus if (view().terminal.opened()) { const id = terminal.active() - if (id && focusTerminalById(id)) return + if (id && shouldFocusTerminalOnKeyDown(event) && focusTerminalById(id)) return } // Only treat explicit scroll keys as potential "user scroll" gestures. diff --git a/packages/app/src/pages/session/helpers.test.ts b/packages/app/src/pages/session/helpers.test.ts index 047946fc1e..95f7cd384d 100644 --- a/packages/app/src/pages/session/helpers.test.ts +++ b/packages/app/src/pages/session/helpers.test.ts @@ -7,6 +7,7 @@ import { createSessionTabs, focusTerminalById, getTabReorderIndex, + shouldFocusTerminalOnKeyDown, } from "./helpers" describe("createOpenReviewFile", () => { @@ -86,6 +87,26 @@ describe("focusTerminalById", () => { }) }) +describe("shouldFocusTerminalOnKeyDown", () => { + test("skips pure modifier keys", () => { + expect(shouldFocusTerminalOnKeyDown(new KeyboardEvent("keydown", { key: "Meta", metaKey: true }))).toBe(false) + expect(shouldFocusTerminalOnKeyDown(new KeyboardEvent("keydown", { key: "Control", ctrlKey: true }))).toBe(false) + expect(shouldFocusTerminalOnKeyDown(new KeyboardEvent("keydown", { key: "Alt", altKey: true }))).toBe(false) + expect(shouldFocusTerminalOnKeyDown(new KeyboardEvent("keydown", { key: "Shift", shiftKey: true }))).toBe(false) + }) + + test("skips shortcut key combos", () => { + expect(shouldFocusTerminalOnKeyDown(new KeyboardEvent("keydown", { key: "c", metaKey: true }))).toBe(false) + expect(shouldFocusTerminalOnKeyDown(new KeyboardEvent("keydown", { key: "c", ctrlKey: true }))).toBe(false) + expect(shouldFocusTerminalOnKeyDown(new KeyboardEvent("keydown", { key: "ArrowLeft", altKey: true }))).toBe(false) + }) + + test("keeps plain typing focused on terminal", () => { + expect(shouldFocusTerminalOnKeyDown(new KeyboardEvent("keydown", { key: "a" }))).toBe(true) + expect(shouldFocusTerminalOnKeyDown(new KeyboardEvent("keydown", { key: "A", shiftKey: true }))).toBe(true) + }) +}) + describe("getTabReorderIndex", () => { test("returns target index for valid drag reorder", () => { expect(getTabReorderIndex(["a", "b", "c"], "a", "c")).toBe(2) diff --git a/packages/app/src/pages/session/helpers.ts b/packages/app/src/pages/session/helpers.ts index c3571f3ffc..7e2c1ccf7b 100644 --- a/packages/app/src/pages/session/helpers.ts +++ b/packages/app/src/pages/session/helpers.ts @@ -93,6 +93,13 @@ export const focusTerminalById = (id: string) => { return true } +const skip = new Set(["Alt", "Control", "Meta", "Shift"]) + +export const shouldFocusTerminalOnKeyDown = (event: Pick) => { + if (skip.has(event.key)) return false + return !(event.ctrlKey || event.metaKey || event.altKey) +} + export const createOpenReviewFile = (input: { showAllFiles: () => void tabForPath: (path: string) => string diff --git a/packages/console/app/src/routes/zen/util/handler.ts b/packages/console/app/src/routes/zen/util/handler.ts index 9dbadf1eef..ab075a24d8 100644 --- a/packages/console/app/src/routes/zen/util/handler.ts +++ b/packages/console/app/src/routes/zen/util/handler.ts @@ -340,6 +340,13 @@ export async function handler( "error.message": error.message, "error.cause": error.cause?.toString(), }) + if (error.message.startsWith("Failed query")) { + try { + logger.metric({ + "error.cause2": JSON.stringify(error.cause), + }) + } catch (e) {} + } // Note: both top level "type" and "error.type" fields are used by the @ai-sdk/anthropic client to render the error message. if ( diff --git a/packages/console/core/script/lookup-user.ts b/packages/console/core/script/lookup-user.ts index 360fc62722..7154f29801 100644 --- a/packages/console/core/script/lookup-user.ts +++ b/packages/console/core/script/lookup-user.ts @@ -1,7 +1,14 @@ import { Database, and, eq, sql } from "../src/drizzle/index.js" import { AuthTable } from "../src/schema/auth.sql.js" import { UserTable } from "../src/schema/user.sql.js" -import { BillingTable, PaymentTable, SubscriptionTable, BlackPlans, UsageTable } from "../src/schema/billing.sql.js" +import { + BillingTable, + PaymentTable, + SubscriptionTable, + BlackPlans, + UsageTable, + LiteTable, +} from "../src/schema/billing.sql.js" import { WorkspaceTable } from "../src/schema/workspace.sql.js" import { KeyTable } from "../src/schema/key.sql.js" import { BlackData } from "../src/black.js" @@ -72,11 +79,13 @@ else { workspaceID: UserTable.workspaceID, workspaceName: WorkspaceTable.name, role: UserTable.role, - subscribed: SubscriptionTable.timeCreated, + black: SubscriptionTable.timeCreated, + lite: LiteTable.timeCreated, }) .from(UserTable) .rightJoin(WorkspaceTable, eq(WorkspaceTable.id, UserTable.workspaceID)) .leftJoin(SubscriptionTable, eq(SubscriptionTable.userID, UserTable.id)) + .leftJoin(LiteTable, eq(LiteTable.userID, UserTable.id)) .where(eq(UserTable.accountID, accountID)) .then((rows) => rows.map((row) => ({ @@ -84,7 +93,8 @@ else { workspaceID: row.workspaceID, workspaceName: row.workspaceName, role: row.role, - subscribed: formatDate(row.subscribed), + black: formatDate(row.black), + lite: formatDate(row.lite), })), ), ) @@ -151,13 +161,14 @@ async function printWorkspace(workspaceID: string) { balance: BillingTable.balance, customerID: BillingTable.customerID, reload: BillingTable.reload, - subscriptionID: BillingTable.subscriptionID, - subscription: { + blackSubscriptionID: BillingTable.subscriptionID, + blackSubscription: { plan: BillingTable.subscriptionPlan, booked: BillingTable.timeSubscriptionBooked, enrichment: BillingTable.subscription, }, - timeSubscriptionSelected: BillingTable.timeSubscriptionSelected, + timeBlackSubscriptionSelected: BillingTable.timeSubscriptionSelected, + liteSubscriptionID: BillingTable.liteSubscriptionID, }) .from(BillingTable) .where(eq(BillingTable.workspaceID, workspace.id)) @@ -167,16 +178,21 @@ async function printWorkspace(workspaceID: string) { balance: `$${(row.balance / 100000000).toFixed(2)}`, reload: row.reload ? "yes" : "no", customerID: row.customerID, - subscriptionID: row.subscriptionID, - subscription: row.subscriptionID + liteSubscriptionID: row.liteSubscriptionID, + blackSubscriptionID: row.blackSubscriptionID, + blackSubscription: row.blackSubscriptionID ? [ - `Black ${row.subscription.enrichment!.plan}`, - row.subscription.enrichment!.seats > 1 ? `X ${row.subscription.enrichment!.seats} seats` : "", - row.subscription.enrichment!.coupon ? `(coupon: ${row.subscription.enrichment!.coupon})` : "", - `(ref: ${row.subscriptionID})`, + `Black ${row.blackSubscription.enrichment!.plan}`, + row.blackSubscription.enrichment!.seats > 1 + ? `X ${row.blackSubscription.enrichment!.seats} seats` + : "", + row.blackSubscription.enrichment!.coupon + ? `(coupon: ${row.blackSubscription.enrichment!.coupon})` + : "", + `(ref: ${row.blackSubscriptionID})`, ].join(" ") - : row.subscription.booked - ? `Waitlist ${row.subscription.plan} plan${row.timeSubscriptionSelected ? " (selected)" : ""}` + : row.blackSubscription.booked + ? `Waitlist ${row.blackSubscription.plan} plan${row.timeBlackSubscriptionSelected ? " (selected)" : ""}` : undefined, }))[0], ), diff --git a/packages/opencode/package.json b/packages/opencode/package.json index 691724dd4c..39b4e6232b 100644 --- a/packages/opencode/package.json +++ b/packages/opencode/package.json @@ -121,7 +121,7 @@ "drizzle-orm": "catalog:", "effect": "catalog:", "fuzzysort": "3.1.0", - "gitlab-ai-provider": "5.2.2", + "gitlab-ai-provider": "5.3.1", "glob": "13.0.5", "google-auth-library": "10.5.0", "gray-matter": "4.0.3", diff --git a/packages/opencode/src/cli/cmd/tui/app.tsx b/packages/opencode/src/cli/cmd/tui/app.tsx index b8031ff910..ac87461ecf 100644 --- a/packages/opencode/src/cli/cmd/tui/app.tsx +++ b/packages/opencode/src/cli/cmd/tui/app.tsx @@ -212,7 +212,7 @@ function App() { const command = useCommandDialog() const sdk = useSDK() const toast = useToast() - const { theme, mode, setMode } = useTheme() + const { theme, mode, setMode, locked, lock, unlock } = useTheme() const sync = useSync() const exit = useExit() const promptRef = usePromptRef() @@ -557,7 +557,7 @@ function App() { category: "System", }, { - title: "Toggle appearance", + title: "Toggle Theme Mode", value: "theme.switch_mode", onSelect: (dialog) => { setMode(mode() === "dark" ? "light" : "dark") @@ -565,6 +565,16 @@ function App() { }, category: "System", }, + { + title: locked() ? "Unlock Theme Mode" : "Lock Theme Mode", + value: "theme.mode.lock", + onSelect: (dialog) => { + if (locked()) unlock() + else lock() + dialog.clear() + }, + category: "System", + }, { title: "Help", value: "help.show", diff --git a/packages/opencode/src/cli/cmd/tui/context/theme.tsx b/packages/opencode/src/cli/cmd/tui/context/theme.tsx index d786e0b491..a3d268afd3 100644 --- a/packages/opencode/src/cli/cmd/tui/context/theme.tsx +++ b/packages/opencode/src/cli/cmd/tui/context/theme.tsx @@ -283,9 +283,15 @@ export const { use: useTheme, provider: ThemeProvider } = createSimpleContext({ const renderer = useRenderer() const config = useTuiConfig() const kv = useKV() + const pick = (value: unknown) => { + if (value === "dark" || value === "light") return value + return + } + const lock = pick(kv.get("theme_mode_lock")) const [store, setStore] = createStore({ themes: DEFAULT_THEMES, - mode: kv.get("theme_mode", props.mode), + mode: lock ?? pick(kv.get("theme_mode", props.mode)) ?? props.mode, + lock, active: (config.theme ?? kv.get("theme", "opencode")) as string, ready: false, }) @@ -345,16 +351,30 @@ export const { use: useTheme, provider: ThemeProvider } = createSimpleContext({ }) } - function update(mode: "dark" | "light") { + function apply(mode: "dark" | "light") { + kv.set("theme_mode", mode) if (store.mode === mode) return setStore("mode", mode) - kv.set("theme_mode", mode) renderer.clearPaletteCache() resolveSystemTheme(mode) } + function pin(mode: "dark" | "light" = store.mode) { + setStore("lock", mode) + kv.set("theme_mode_lock", mode) + apply(mode) + } + + function free() { + setStore("lock", undefined) + kv.set("theme_mode_lock", undefined) + const mode = renderer.themeMode + if (mode) apply(mode) + } + const handle = (mode: "dark" | "light") => { - update(mode) + if (store.lock) return + apply(mode) } renderer.on(CliRenderEvents.THEME_MODE, handle) onCleanup(() => { @@ -390,8 +410,17 @@ export const { use: useTheme, provider: ThemeProvider } = createSimpleContext({ mode() { return store.mode }, + locked() { + return store.lock !== undefined + }, + lock() { + pin(store.mode) + }, + unlock() { + free() + }, setMode(mode: "dark" | "light") { - update(mode) + pin(mode) }, set(theme: string) { setStore("active", theme)