diff --git a/packages/app/e2e/session/session-review.spec.ts b/packages/app/e2e/session/session-review.spec.ts index 28c85edb0b..c0421f0283 100644 --- a/packages/app/e2e/session/session-review.spec.ts +++ b/packages/app/e2e/session/session-review.spec.ts @@ -123,6 +123,101 @@ async function spot(page: Parameters[0]["page"], file: string) { }, file) } +async function comment(page: Parameters[0]["page"], file: string, note: string) { + const row = page.locator(`[data-file="${file}"]`).first() + await expect(row).toBeVisible() + + const line = row.locator('diffs-container [data-line="2"]').first() + await expect(line).toBeVisible() + await line.hover() + + const add = row.getByRole("button", { name: /^Comment$/ }).first() + await expect(add).toBeVisible() + await add.click() + + const area = row.locator('[data-slot="line-comment-textarea"]').first() + await expect(area).toBeVisible() + await area.fill(note) + + const submit = row.locator('[data-slot="line-comment-action"][data-variant="primary"]').first() + await expect(submit).toBeEnabled() + await submit.click() + + await expect(row.locator('[data-slot="line-comment-content"]').filter({ hasText: note }).first()).toBeVisible() + await expect(row.locator('[data-slot="line-comment-tools"]').first()).toBeVisible() +} + +async function overflow(page: Parameters[0]["page"], file: string) { + const row = page.locator(`[data-file="${file}"]`).first() + const view = page.locator('[data-slot="session-review-scroll"] .scroll-view__viewport').first() + const pop = row.locator('[data-slot="line-comment-popover"][data-inline-body]').first() + const tools = row.locator('[data-slot="line-comment-tools"]').first() + + const [width, viewBox, popBox, toolsBox] = await Promise.all([ + view.evaluate((el) => el.scrollWidth - el.clientWidth), + view.boundingBox(), + pop.boundingBox(), + tools.boundingBox(), + ]) + + if (!viewBox || !popBox || !toolsBox) return null + + return { + width, + pop: popBox.x + popBox.width - (viewBox.x + viewBox.width), + tools: toolsBox.x + toolsBox.width - (viewBox.x + viewBox.width), + } +} + +test("review applies inline comment clicks without horizontal overflow", async ({ page, withProject }) => { + test.setTimeout(180_000) + + const tag = `review-comment-${Date.now()}` + const file = `review-comment-${tag}.txt` + const note = `comment ${tag}` + + await page.setViewportSize({ width: 1280, height: 900 }) + + await withProject(async (project) => { + const sdk = createSdk(project.directory) + + await withSession(sdk, `e2e review comment ${tag}`, async (session) => { + await patch(sdk, session.id, seed([{ file, mark: tag }])) + + await expect + .poll( + async () => { + const diff = await sdk.session.diff({ sessionID: session.id }).then((res) => res.data ?? []) + return diff.length + }, + { timeout: 60_000 }, + ) + .toBe(1) + + await project.gotoSession(session.id) + await show(page) + + const tab = page.getByRole("tab", { name: /Review/i }).first() + await expect(tab).toBeVisible() + await tab.click() + + await expand(page) + await waitMark(page, file, tag) + await comment(page, file, note) + + await expect + .poll(async () => (await overflow(page, file))?.width ?? Number.POSITIVE_INFINITY, { timeout: 10_000 }) + .toBeLessThanOrEqual(1) + await expect + .poll(async () => (await overflow(page, file))?.pop ?? Number.POSITIVE_INFINITY, { timeout: 10_000 }) + .toBeLessThanOrEqual(1) + await expect + .poll(async () => (await overflow(page, file))?.tools ?? Number.POSITIVE_INFINITY, { timeout: 10_000 }) + .toBeLessThanOrEqual(1) + }) + }) +}) + test("review keeps scroll position after a live diff update", async ({ page, withProject }) => { test.skip(Boolean(process.env.CI), "Flaky in CI for now.") test.setTimeout(180_000) diff --git a/packages/console/core/script/lookup-user.ts b/packages/console/core/script/lookup-user.ts index 0dfda24116..8b7bbc1caa 100644 --- a/packages/console/core/script/lookup-user.ts +++ b/packages/console/core/script/lookup-user.ts @@ -3,6 +3,7 @@ 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 { WorkspaceTable } from "../src/schema/workspace.sql.js" +import { KeyTable } from "../src/schema/key.sql.js" import { BlackData } from "../src/black.js" import { centsToMicroCents } from "../src/util/price.js" import { getWeekBounds } from "../src/util/date.js" @@ -10,13 +11,31 @@ import { getWeekBounds } from "../src/util/date.js" // get input from command line const identifier = process.argv[2] if (!identifier) { - console.error("Usage: bun lookup-user.ts ") + console.error("Usage: bun lookup-user.ts ") process.exit(1) } +// loop up by workspace ID if (identifier.startsWith("wrk_")) { await printWorkspace(identifier) -} else { +} +// lookup by API key +else if (identifier.startsWith("key_")) { + const key = await Database.use((tx) => + tx + .select() + .from(KeyTable) + .where(eq(KeyTable.id, identifier)) + .then((rows) => rows[0]), + ) + if (!key) { + console.error("API key not found") + process.exit(1) + } + await printWorkspace(key.workspaceID) +} +// lookup by email +else { const authData = await Database.use(async (tx) => tx.select().from(AuthTable).where(eq(AuthTable.subject, identifier)), ) diff --git a/packages/ui/src/components/line-comment-styles.ts b/packages/ui/src/components/line-comment-styles.ts index d5be675541..8fd02f0881 100644 --- a/packages/ui/src/components/line-comment-styles.ts +++ b/packages/ui/src/components/line-comment-styles.ts @@ -15,6 +15,7 @@ export const lineCommentStyles = ` right: auto; display: flex; width: 100%; + min-width: 0; align-items: flex-start; } @@ -64,6 +65,7 @@ export const lineCommentStyles = ` z-index: var(--line-comment-popover-z, 40); min-width: 200px; max-width: none; + box-sizing: border-box; border-radius: 8px; background: var(--surface-raised-stronger-non-alpha); box-shadow: var(--shadow-xxs-border); @@ -75,9 +77,10 @@ export const lineCommentStyles = ` top: auto; right: auto; margin-left: 8px; - flex: 0 1 600px; - width: min(100%, 600px); - max-width: min(100%, 600px); + flex: 1 1 0%; + width: auto; + max-width: 100%; + min-width: 0; } [data-component="line-comment"][data-inline] [data-slot="line-comment-popover"][data-inline-body] { @@ -96,23 +99,27 @@ export const lineCommentStyles = ` } [data-component="line-comment"][data-inline][data-variant="editor"] [data-slot="line-comment-popover"] { - flex-basis: 600px; + width: 100%; } [data-component="line-comment"] [data-slot="line-comment-content"] { display: flex; flex-direction: column; gap: 6px; + width: 100%; + min-width: 0; } [data-component="line-comment"] [data-slot="line-comment-head"] { display: flex; align-items: flex-start; gap: 8px; + min-width: 0; } [data-component="line-comment"] [data-slot="line-comment-text"] { flex: 1; + min-width: 0; font-family: var(--font-family-sans); font-size: var(--font-size-base); font-weight: var(--font-weight-regular); @@ -120,6 +127,7 @@ export const lineCommentStyles = ` letter-spacing: var(--letter-spacing-normal); color: var(--text-strong); white-space: pre-wrap; + overflow-wrap: anywhere; } [data-component="line-comment"] [data-slot="line-comment-tools"] { @@ -127,6 +135,7 @@ export const lineCommentStyles = ` display: flex; align-items: center; justify-content: flex-end; + min-width: 0; } [data-component="line-comment"] [data-slot="line-comment-label"], @@ -137,17 +146,22 @@ export const lineCommentStyles = ` line-height: var(--line-height-large); letter-spacing: var(--letter-spacing-normal); color: var(--text-weak); - white-space: nowrap; + min-width: 0; + white-space: normal; + overflow-wrap: anywhere; } [data-component="line-comment"] [data-slot="line-comment-editor"] { display: flex; flex-direction: column; gap: 8px; + width: 100%; + min-width: 0; } [data-component="line-comment"] [data-slot="line-comment-textarea"] { width: 100%; + box-sizing: border-box; resize: vertical; padding: 8px; border-radius: var(--radius-md); @@ -167,11 +181,14 @@ export const lineCommentStyles = ` [data-component="line-comment"] [data-slot="line-comment-actions"] { display: flex; align-items: center; + flex-wrap: wrap; gap: 8px; padding-left: 8px; + min-width: 0; } [data-component="line-comment"] [data-slot="line-comment-editor-label"] { + flex: 1 1 220px; margin-right: auto; } diff --git a/packages/ui/src/components/line-comment.tsx b/packages/ui/src/components/line-comment.tsx index ff5d1df007..bc47ad9405 100644 --- a/packages/ui/src/components/line-comment.tsx +++ b/packages/ui/src/components/line-comment.tsx @@ -206,6 +206,16 @@ export const LineCommentEditor = (props: LineCommentEditorProps) => { const [text, setText] = createSignal(split.value) const focus = () => refs.textarea?.focus() + const hold: JSX.EventHandlerUnion = (e) => { + e.preventDefault() + e.stopPropagation() + } + const click = + (fn: VoidFunction): JSX.EventHandlerUnion => + (e) => { + e.stopPropagation() + fn() + } createEffect(() => { setText(split.value) @@ -268,7 +278,8 @@ export const LineCommentEditor = (props: LineCommentEditorProps) => { type="button" data-slot="line-comment-action" data-variant="ghost" - on:click={split.onCancel as any} + on:mousedown={hold as any} + on:click={click(split.onCancel) as any} > {split.cancelLabel ?? i18n.t("ui.common.cancel")} @@ -277,7 +288,8 @@ export const LineCommentEditor = (props: LineCommentEditorProps) => { data-slot="line-comment-action" data-variant="primary" disabled={text().trim().length === 0} - on:click={submit as any} + on:mousedown={hold as any} + on:click={click(submit) as any} > {split.submitLabel ?? i18n.t("ui.lineComment.submit")} diff --git a/packages/web/src/content/docs/agents.mdx b/packages/web/src/content/docs/agents.mdx index 8764642124..5522f77aae 100644 --- a/packages/web/src/content/docs/agents.mdx +++ b/packages/web/src/content/docs/agents.mdx @@ -366,9 +366,11 @@ The model ID in your OpenCode config uses the format `provider/model-id`. For ex --- -### Tools +### Tools (deprecated) -Control which tools are available in this agent with the `tools` config. You can enable or disable specific tools by setting them to `true` or `false`. +`tools` is **deprecated**. Prefer the agent's [`permission`](#permissions) field for new configs, updates and more fine-grained control. + +Allows you to control which tools are available in this agent. You can enable or disable specific tools by setting them to `true` or `false`. In an agent's `tools` config, `true` is equivalent to `{"*": "allow"}` permission and `false` is equivalent to `{"*": "deny"}` permission. ```json title="opencode.json" {3-6,9-12} { @@ -392,7 +394,7 @@ Control which tools are available in this agent with the `tools` config. You can The agent-specific config overrides the global config. ::: -You can also use wildcards to control multiple tools at once. For example, to disable all tools from an MCP server: +You can also use wildcards in legacy `tools` entries to control multiple tools at once. For example, to disable all tools from an MCP server: ```json title="opencode.json" { diff --git a/packages/web/src/content/docs/zen.mdx b/packages/web/src/content/docs/zen.mdx index abbd8058ea..4a4691d4fe 100644 --- a/packages/web/src/content/docs/zen.mdx +++ b/packages/web/src/content/docs/zen.mdx @@ -86,7 +86,6 @@ You can also access our models through the following API endpoints. | Claude Haiku 4.5 | claude-haiku-4-5 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | | Claude Haiku 3.5 | claude-3-5-haiku | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | | Gemini 3.1 Pro | gemini-3.1-pro | `https://opencode.ai/zen/v1/models/gemini-3.1-pro` | `@ai-sdk/google` | -| Gemini 3 Pro | gemini-3-pro | `https://opencode.ai/zen/v1/models/gemini-3-pro` | `@ai-sdk/google` | | Gemini 3 Flash | gemini-3-flash | `https://opencode.ai/zen/v1/models/gemini-3-flash` | `@ai-sdk/google` | | MiniMax M2.5 | minimax-m2.5 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | | MiniMax M2.5 Free | minimax-m2.5-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | @@ -95,8 +94,6 @@ You can also access our models through the following API endpoints. | GLM 4.7 | glm-4.7 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | | GLM 4.6 | glm-4.6 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | | Kimi K2.5 | kimi-k2.5 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| Kimi K2 Thinking | kimi-k2-thinking | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| Kimi K2 | kimi-k2 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | | Qwen3 Coder 480B | qwen3-coder | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | | Big Pickle | big-pickle | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | | MiMo V2 Flash Free | mimo-v2-flash-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | @@ -134,8 +131,6 @@ We support a pay-as-you-go model. Below are the prices **per 1M tokens**. | GLM 4.7 | $0.60 | $2.20 | $0.10 | - | | GLM 4.6 | $0.60 | $2.20 | $0.10 | - | | Kimi K2.5 | $0.60 | $3.00 | $0.10 | - | -| Kimi K2 Thinking | $0.40 | $2.50 | - | - | -| Kimi K2 | $0.40 | $2.50 | - | - | | Qwen3 Coder 480B | $0.45 | $1.50 | - | - | | Claude Opus 4.6 | $5.00 | $25.00 | $0.50 | $6.25 | | Claude Opus 4.5 | $5.00 | $25.00 | $0.50 | $6.25 | @@ -149,8 +144,6 @@ We support a pay-as-you-go model. Below are the prices **per 1M tokens**. | Claude Haiku 3.5 | $0.80 | $4.00 | $0.08 | $1.00 | | Gemini 3.1 Pro (≤ 200K tokens) | $2.00 | $12.00 | $0.20 | - | | Gemini 3.1 Pro (> 200K tokens) | $4.00 | $18.00 | $0.40 | - | -| Gemini 3 Pro (≤ 200K tokens) | $2.00 | $12.00 | $0.20 | - | -| Gemini 3 Pro (> 200K tokens) | $4.00 | $18.00 | $0.40 | - | | Gemini 3 Flash | $0.50 | $3.00 | $0.05 | - | | GPT 5.4 Pro | $30.00 | $180.00 | $30.00 | - | | GPT 5.4 | $2.50 | $15.00 | $0.25 | - | @@ -206,12 +199,13 @@ charging you more than $20 if your balance goes below $5. | Model | Deprecation date | | ---------------- | ---------------- | -| Qwen3 Coder 480B | Feb 6, 2026 | -| Kimi K2 Thinking | March 6, 2026 | -| Kimi K2 | March 6, 2026 | | MiniMax M2.1 | March 15, 2026 | | GLM 4.7 | March 15, 2026 | | GLM 4.6 | March 15, 2026 | +| Gemini 3 Pro | March 9, 2026 | +| Kimi K2 Thinking | March 6, 2026 | +| Kimi K2 | March 6, 2026 | +| Qwen3 Coder 480B | Feb 6, 2026 | ---