mirror of
https://github.com/anomalyco/opencode.git
synced 2026-02-21 00:04:22 +00:00
Compare commits
7 Commits
production
...
add-api-sh
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e6a49ed85c | ||
|
|
77cdfcdb64 | ||
|
|
950df3de19 | ||
|
|
1d9f05e4f5 | ||
|
|
46361cf35c | ||
|
|
c09d3dd5a7 | ||
|
|
fe89bedfcc |
@@ -5,8 +5,16 @@ import DESCRIPTION from "./github-triage.txt"
|
||||
const TEAM = {
|
||||
desktop: ["adamdotdevin", "iamdavidhill", "Brendonovich", "nexxeln"],
|
||||
zen: ["fwang", "MrMushrooooom"],
|
||||
tui: ["thdxr", "kommander", "rekram1-node"],
|
||||
core: ["thdxr", "rekram1-node", "jlongster"],
|
||||
tui: [
|
||||
"thdxr",
|
||||
"kommander",
|
||||
// "rekram1-node" (on vacation)
|
||||
],
|
||||
core: [
|
||||
"thdxr",
|
||||
// "rekram1-node", (on vacation)
|
||||
"jlongster",
|
||||
],
|
||||
docs: ["R44VC0RP"],
|
||||
windows: ["Hona"],
|
||||
} as const
|
||||
@@ -42,10 +50,7 @@ async function githubFetch(endpoint: string, options: RequestInit = {}) {
|
||||
export default tool({
|
||||
description: DESCRIPTION,
|
||||
args: {
|
||||
assignee: tool.schema
|
||||
.enum(ASSIGNEES as [string, ...string[]])
|
||||
.describe("The username of the assignee")
|
||||
.default("rekram1-node"),
|
||||
assignee: tool.schema.enum(ASSIGNEES as [string, ...string[]]).describe("The username of the assignee"),
|
||||
labels: tool.schema
|
||||
.array(tool.schema.enum(["nix", "opentui", "perf", "web", "desktop", "zen", "docs", "windows", "core"]))
|
||||
.describe("The labels(s) to add to the issue")
|
||||
@@ -68,7 +73,8 @@ export default tool({
|
||||
results.push("Dropped label: nix (issue does not mention nix)")
|
||||
}
|
||||
|
||||
const assignee = nix ? "rekram1-node" : web ? pick(TEAM.desktop) : args.assignee
|
||||
// const assignee = nix ? "rekram1-node" : web ? pick(TEAM.desktop) : args.assignee
|
||||
const assignee = web ? pick(TEAM.desktop) : args.assignee
|
||||
|
||||
if (labels.includes("zen") && !zen) {
|
||||
throw new Error("Only add the zen label when issue title/body contains 'zen'")
|
||||
|
||||
@@ -4,3 +4,5 @@ Choose labels and assignee using the current triage policy and ownership rules.
|
||||
Pick the most fitting labels for the issue and assign one owner.
|
||||
|
||||
If unsure, choose the team/section with the most overlap with the issue and assign a member from that team at random.
|
||||
|
||||
(Note: rekram1-node is on vacation, do not assign issues to him.)
|
||||
|
||||
@@ -225,7 +225,7 @@ export async function hoverSessionItem(page: Page, sessionID: string) {
|
||||
export async function openSessionMoreMenu(page: Page, sessionID: string) {
|
||||
await expect(page).toHaveURL(new RegExp(`/session/${sessionID}(?:[/?#]|$)`))
|
||||
|
||||
const scroller = page.locator(".session-scroller").first()
|
||||
const scroller = page.locator(".scroll-view__viewport").first()
|
||||
await expect(scroller).toBeVisible()
|
||||
await expect(scroller.getByRole("heading", { level: 1 }).first()).toBeVisible({ timeout: 30_000 })
|
||||
|
||||
|
||||
@@ -44,7 +44,7 @@ test("session can be renamed via header menu", async ({ page, sdk, gotoSession }
|
||||
const menu = await openSessionMoreMenu(page, session.id)
|
||||
await clickMenuItem(menu, /rename/i)
|
||||
|
||||
const input = page.locator(".session-scroller").locator(inlineInputSelector).first()
|
||||
const input = page.locator(".scroll-view__viewport").locator(inlineInputSelector).first()
|
||||
await expect(input).toBeVisible()
|
||||
await expect(input).toBeFocused()
|
||||
await input.fill(renamedTitle)
|
||||
|
||||
@@ -11,6 +11,7 @@ import { Accordion } from "@opencode-ai/ui/accordion"
|
||||
import { StickyAccordionHeader } from "@opencode-ai/ui/sticky-accordion-header"
|
||||
import { Code } from "@opencode-ai/ui/code"
|
||||
import { Markdown } from "@opencode-ai/ui/markdown"
|
||||
import { ScrollView } from "@opencode-ai/ui/scroll-view"
|
||||
import type { Message, Part, UserMessage } from "@opencode-ai/sdk/v2/client"
|
||||
import { useLanguage } from "@/context/language"
|
||||
import { getSessionContextMetrics } from "./session-context-metrics"
|
||||
@@ -268,9 +269,9 @@ export function SessionContextTab() {
|
||||
})
|
||||
|
||||
return (
|
||||
<div
|
||||
class="@container h-full overflow-y-auto no-scrollbar pb-10"
|
||||
ref={(el) => {
|
||||
<ScrollView
|
||||
class="@container h-full pb-10"
|
||||
viewportRef={(el) => {
|
||||
scroll = el
|
||||
restoreScroll()
|
||||
}}
|
||||
@@ -336,6 +337,6 @@ export function SessionContextTab() {
|
||||
</Accordion>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ScrollView>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -62,7 +62,7 @@ export const SessionQuestionDock: Component<{ request: QuestionRequest; onSubmit
|
||||
const measure = () => {
|
||||
if (!root) return
|
||||
|
||||
const scroller = document.querySelector(".session-scroller")
|
||||
const scroller = document.querySelector(".scroll-view__viewport")
|
||||
const head = scroller instanceof HTMLElement ? scroller.firstElementChild : undefined
|
||||
const top =
|
||||
head instanceof HTMLElement && head.classList.contains("sticky") ? head.getBoundingClientRect().bottom : 0
|
||||
@@ -95,7 +95,7 @@ export const SessionQuestionDock: Component<{ request: QuestionRequest; onSubmit
|
||||
window.addEventListener("resize", update)
|
||||
|
||||
const dock = root?.closest('[data-component="session-prompt-dock"]')
|
||||
const scroller = document.querySelector(".session-scroller")
|
||||
const scroller = document.querySelector(".scroll-view__viewport")
|
||||
const observer = new ResizeObserver(update)
|
||||
if (dock instanceof HTMLElement) observer.observe(dock)
|
||||
if (scroller instanceof HTMLElement) observer.observe(scroller)
|
||||
|
||||
@@ -9,6 +9,7 @@ import { showToast } from "@opencode-ai/ui/toast"
|
||||
import { LineComment as LineCommentView, LineCommentEditor } from "@opencode-ai/ui/line-comment"
|
||||
import { Mark } from "@opencode-ai/ui/logo"
|
||||
import { Tabs } from "@opencode-ai/ui/tabs"
|
||||
import { ScrollView } from "@opencode-ai/ui/scroll-view"
|
||||
import { useLayout } from "@/context/layout"
|
||||
import { selectionFromLines, useFile, type FileSelection, type SelectedLineRange } from "@/context/file"
|
||||
import { useComments } from "@/context/comments"
|
||||
@@ -509,51 +510,52 @@ export function FileTabContent(props: { tab: string }) {
|
||||
)
|
||||
|
||||
return (
|
||||
<Tabs.Content
|
||||
value={props.tab}
|
||||
class="mt-3 relative"
|
||||
ref={(el: HTMLDivElement) => {
|
||||
scroll = el
|
||||
restoreScroll()
|
||||
}}
|
||||
onScroll={handleScroll}
|
||||
>
|
||||
<Switch>
|
||||
<Match when={state()?.loaded && isImage()}>
|
||||
<div class="px-6 py-4 pb-40">
|
||||
<img
|
||||
src={imageDataUrl()}
|
||||
alt={path()}
|
||||
class="max-w-full"
|
||||
onLoad={() => requestAnimationFrame(restoreScroll)}
|
||||
/>
|
||||
</div>
|
||||
</Match>
|
||||
<Match when={state()?.loaded && isSvg()}>
|
||||
<div class="flex flex-col gap-4 px-6 py-4">
|
||||
{renderCode(svgContent() ?? "", "")}
|
||||
<Show when={svgPreviewUrl()}>
|
||||
<div class="flex justify-center pb-40">
|
||||
<img src={svgPreviewUrl()} alt={path()} class="max-w-full max-h-96" />
|
||||
</div>
|
||||
</Show>
|
||||
</div>
|
||||
</Match>
|
||||
<Match when={state()?.loaded && isBinary()}>
|
||||
<div class="h-full px-6 pb-42 flex flex-col items-center justify-center text-center gap-6">
|
||||
<Mark class="w-14 opacity-10" />
|
||||
<div class="flex flex-col gap-2 max-w-md">
|
||||
<div class="text-14-semibold text-text-strong truncate">{path()?.split("/").pop()}</div>
|
||||
<div class="text-14-regular text-text-weak">{language.t("session.files.binaryContent")}</div>
|
||||
<Tabs.Content value={props.tab} class="mt-3 relative h-full">
|
||||
<ScrollView
|
||||
class="h-full"
|
||||
viewportRef={(el: HTMLDivElement) => {
|
||||
scroll = el
|
||||
restoreScroll()
|
||||
}}
|
||||
onScroll={handleScroll as any}
|
||||
>
|
||||
<Switch>
|
||||
<Match when={state()?.loaded && isImage()}>
|
||||
<div class="px-6 py-4 pb-40">
|
||||
<img
|
||||
src={imageDataUrl()}
|
||||
alt={path()}
|
||||
class="max-w-full"
|
||||
onLoad={() => requestAnimationFrame(restoreScroll)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Match>
|
||||
<Match when={state()?.loaded}>{renderCode(contents(), "pb-40")}</Match>
|
||||
<Match when={state()?.loading}>
|
||||
<div class="px-6 py-4 text-text-weak">{language.t("common.loading")}...</div>
|
||||
</Match>
|
||||
<Match when={state()?.error}>{(err) => <div class="px-6 py-4 text-text-weak">{err()}</div>}</Match>
|
||||
</Switch>
|
||||
</Match>
|
||||
<Match when={state()?.loaded && isSvg()}>
|
||||
<div class="flex flex-col gap-4 px-6 py-4">
|
||||
{renderCode(svgContent() ?? "", "")}
|
||||
<Show when={svgPreviewUrl()}>
|
||||
<div class="flex justify-center pb-40">
|
||||
<img src={svgPreviewUrl()} alt={path()} class="max-w-full max-h-96" />
|
||||
</div>
|
||||
</Show>
|
||||
</div>
|
||||
</Match>
|
||||
<Match when={state()?.loaded && isBinary()}>
|
||||
<div class="h-full px-6 pb-42 flex flex-col items-center justify-center text-center gap-6">
|
||||
<Mark class="w-14 opacity-10" />
|
||||
<div class="flex flex-col gap-2 max-w-md">
|
||||
<div class="text-14-semibold text-text-strong truncate">{path()?.split("/").pop()}</div>
|
||||
<div class="text-14-regular text-text-weak">{language.t("session.files.binaryContent")}</div>
|
||||
</div>
|
||||
</div>
|
||||
</Match>
|
||||
<Match when={state()?.loaded}>{renderCode(contents(), "pb-40")}</Match>
|
||||
<Match when={state()?.loading}>
|
||||
<div class="px-6 py-4 text-text-weak">{language.t("common.loading")}...</div>
|
||||
</Match>
|
||||
<Match when={state()?.error}>{(err) => <div class="px-6 py-4 text-text-weak">{err()}</div>}</Match>
|
||||
</Switch>
|
||||
</ScrollView>
|
||||
</Tabs.Content>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ import { DropdownMenu } from "@opencode-ai/ui/dropdown-menu"
|
||||
import { Dialog } from "@opencode-ai/ui/dialog"
|
||||
import { InlineInput } from "@opencode-ai/ui/inline-input"
|
||||
import { SessionTurn } from "@opencode-ai/ui/session-turn"
|
||||
import { ScrollView } from "@opencode-ai/ui/scroll-view"
|
||||
import type { UserMessage } from "@opencode-ai/sdk/v2"
|
||||
import { showToast } from "@opencode-ai/ui/toast"
|
||||
import { shouldMarkBoundaryGesture, normalizeWheelDelta } from "@/pages/session/message-gesture"
|
||||
@@ -322,8 +323,8 @@ export function MessageTimeline(props: {
|
||||
<Icon name="arrow-down-to-line" />
|
||||
</button>
|
||||
</div>
|
||||
<div
|
||||
ref={props.setScrollRef}
|
||||
<ScrollView
|
||||
viewportRef={props.setScrollRef}
|
||||
onWheel={(e) => {
|
||||
const root = e.currentTarget
|
||||
const delta = normalizeWheelDelta({
|
||||
@@ -367,7 +368,7 @@ export function MessageTimeline(props: {
|
||||
if (props.isDesktop) props.onScrollSpyScroll()
|
||||
}}
|
||||
onClick={props.onAutoScrollInteraction}
|
||||
class="relative min-w-0 w-full h-full overflow-y-auto session-scroller"
|
||||
class="relative min-w-0 w-full h-full"
|
||||
style={{
|
||||
"--session-title-height": showHeader() ? "40px" : "0px",
|
||||
"--sticky-accordion-top": showHeader() ? "48px" : "0px",
|
||||
@@ -548,7 +549,7 @@ export function MessageTimeline(props: {
|
||||
)}
|
||||
</For>
|
||||
</div>
|
||||
</div>
|
||||
</ScrollView>
|
||||
</div>
|
||||
</Show>
|
||||
)
|
||||
|
||||
@@ -143,9 +143,9 @@ export function SessionReviewTab(props: SessionReviewTabProps) {
|
||||
open={props.view().review.open()}
|
||||
onOpenChange={props.view().review.setOpen}
|
||||
classes={{
|
||||
root: props.classes?.root ?? "pb-6",
|
||||
root: props.classes?.root ?? "pb-6 pr-3",
|
||||
header: props.classes?.header ?? "px-3",
|
||||
container: props.classes?.container ?? "px-3",
|
||||
container: props.classes?.container ?? "pl-3",
|
||||
}}
|
||||
diffs={props.diffs()}
|
||||
diffStyle={props.diffStyle}
|
||||
|
||||
@@ -25,6 +25,12 @@ if (envPath) {
|
||||
const scriptPath = fs.realpathSync(__filename)
|
||||
const scriptDir = path.dirname(scriptPath)
|
||||
|
||||
//
|
||||
const cached = path.join(scriptDir, ".opencode")
|
||||
if (fs.existsSync(cached)) {
|
||||
run(cached)
|
||||
}
|
||||
|
||||
const platformMap = {
|
||||
darwin: "darwin",
|
||||
linux: "linux",
|
||||
|
||||
@@ -109,8 +109,14 @@ async function main() {
|
||||
// On non-Windows platforms, just verify the binary package exists
|
||||
// Don't replace the wrapper script - it handles binary execution
|
||||
const { binaryPath } = findBinary()
|
||||
console.log(`Platform binary verified at: ${binaryPath}`)
|
||||
console.log("Wrapper script will handle binary execution")
|
||||
const target = path.join(__dirname, "bin", ".opencode")
|
||||
if (fs.existsSync(target)) fs.unlinkSync(target)
|
||||
try {
|
||||
fs.linkSync(binaryPath, target)
|
||||
} catch {
|
||||
fs.copyFileSync(binaryPath, target)
|
||||
}
|
||||
fs.chmodSync(target, 0o755)
|
||||
} catch (error) {
|
||||
console.error("Failed to setup opencode binary:", error.message)
|
||||
process.exit(1)
|
||||
|
||||
@@ -65,7 +65,13 @@ export namespace ModelsDev {
|
||||
status: z.enum(["alpha", "beta", "deprecated"]).optional(),
|
||||
options: z.record(z.string(), z.any()),
|
||||
headers: z.record(z.string(), z.string()).optional(),
|
||||
provider: z.object({ npm: z.string().optional(), api: z.string().optional() }).optional(),
|
||||
provider: z
|
||||
.object({
|
||||
npm: z.string().optional(),
|
||||
api: z.string().optional(),
|
||||
shape: z.enum(["responses", "completions"]).optional(),
|
||||
})
|
||||
.optional(),
|
||||
variants: z.record(z.string(), z.record(z.string(), z.any())).optional(),
|
||||
})
|
||||
export type Model = z.infer<typeof Model>
|
||||
|
||||
@@ -109,7 +109,7 @@ export namespace Provider {
|
||||
"@ai-sdk/github-copilot": createGitHubCopilotOpenAICompatible,
|
||||
}
|
||||
|
||||
type CustomModelLoader = (sdk: any, modelID: string, options?: Record<string, any>) => Promise<any>
|
||||
type CustomModelLoader = (sdk: any, model: Model, options?: Record<string, any>) => Promise<any>
|
||||
type CustomLoader = (provider: Info) => Promise<{
|
||||
autoload: boolean
|
||||
getModel?: CustomModelLoader
|
||||
@@ -153,8 +153,9 @@ export namespace Provider {
|
||||
openai: async () => {
|
||||
return {
|
||||
autoload: false,
|
||||
async getModel(sdk: any, modelID: string, _options?: Record<string, any>) {
|
||||
return sdk.responses(modelID)
|
||||
async getModel(sdk: any, model: Model, _options?: Record<string, any>) {
|
||||
if (model.api.shape === "completions") return sdk.chat(model.api.id)
|
||||
return sdk.responses(model.api.id)
|
||||
},
|
||||
options: {},
|
||||
}
|
||||
@@ -162,9 +163,12 @@ export namespace Provider {
|
||||
"github-copilot": async () => {
|
||||
return {
|
||||
autoload: false,
|
||||
async getModel(sdk: any, modelID: string, _options?: Record<string, any>) {
|
||||
if (sdk.responses === undefined && sdk.chat === undefined) return sdk.languageModel(modelID)
|
||||
return shouldUseCopilotResponsesApi(modelID) ? sdk.responses(modelID) : sdk.chat(modelID)
|
||||
async getModel(sdk: any, model: Model, _options?: Record<string, any>) {
|
||||
const shape = model.api.shape
|
||||
if (sdk.responses === undefined && sdk.chat === undefined) return sdk.languageModel(model.api.id)
|
||||
if (shape === "responses") return sdk.responses(model.api.id)
|
||||
if (shape === "completions") return sdk.chat(model.api.id)
|
||||
return shouldUseCopilotResponsesApi(model.api.id) ? sdk.responses(model.api.id) : sdk.chat(model.api.id)
|
||||
},
|
||||
options: {},
|
||||
}
|
||||
@@ -172,9 +176,12 @@ export namespace Provider {
|
||||
"github-copilot-enterprise": async () => {
|
||||
return {
|
||||
autoload: false,
|
||||
async getModel(sdk: any, modelID: string, _options?: Record<string, any>) {
|
||||
if (sdk.responses === undefined && sdk.chat === undefined) return sdk.languageModel(modelID)
|
||||
return shouldUseCopilotResponsesApi(modelID) ? sdk.responses(modelID) : sdk.chat(modelID)
|
||||
async getModel(sdk: any, model: Model, _options?: Record<string, any>) {
|
||||
const shape = model.api.shape
|
||||
if (sdk.responses === undefined && sdk.chat === undefined) return sdk.languageModel(model.api.id)
|
||||
if (shape === "responses") return sdk.responses(model.api.id)
|
||||
if (shape === "completions") return sdk.chat(model.api.id)
|
||||
return shouldUseCopilotResponsesApi(model.api.id) ? sdk.responses(model.api.id) : sdk.chat(model.api.id)
|
||||
},
|
||||
options: {},
|
||||
}
|
||||
@@ -182,12 +189,12 @@ export namespace Provider {
|
||||
azure: async () => {
|
||||
return {
|
||||
autoload: false,
|
||||
async getModel(sdk: any, modelID: string, options?: Record<string, any>) {
|
||||
if (options?.["useCompletionUrls"]) {
|
||||
return sdk.chat(modelID)
|
||||
} else {
|
||||
return sdk.responses(modelID)
|
||||
}
|
||||
async getModel(sdk: any, model: Model, options?: Record<string, any>) {
|
||||
if (sdk.responses === undefined || sdk.chat === undefined) return sdk.languageModel(model.api.id)
|
||||
if (model.api.shape === "completions") return sdk.chat(model.api.id)
|
||||
if (model.api.shape === "responses") return sdk.responses(model.api.id)
|
||||
if (options?.["useCompletionUrls"]) return sdk.chat(model.api.id)
|
||||
return sdk.responses(model.api.id)
|
||||
},
|
||||
options: {},
|
||||
}
|
||||
@@ -196,12 +203,12 @@ export namespace Provider {
|
||||
const resourceName = Env.get("AZURE_COGNITIVE_SERVICES_RESOURCE_NAME")
|
||||
return {
|
||||
autoload: false,
|
||||
async getModel(sdk: any, modelID: string, options?: Record<string, any>) {
|
||||
if (options?.["useCompletionUrls"]) {
|
||||
return sdk.chat(modelID)
|
||||
} else {
|
||||
return sdk.responses(modelID)
|
||||
}
|
||||
async getModel(sdk: any, model: Model, options?: Record<string, any>) {
|
||||
if (sdk.responses === undefined || sdk.chat === undefined) return sdk.languageModel(model.api.id)
|
||||
if (model.api.shape === "completions") return sdk.chat(model.api.id)
|
||||
if (model.api.shape === "responses") return sdk.responses(model.api.id)
|
||||
if (options?.["useCompletionUrls"]) return sdk.chat(model.api.id)
|
||||
return sdk.responses(model.api.id)
|
||||
},
|
||||
options: {
|
||||
baseURL: resourceName ? `https://${resourceName}.cognitiveservices.azure.com/openai` : undefined,
|
||||
@@ -269,7 +276,8 @@ export namespace Provider {
|
||||
return {
|
||||
autoload: true,
|
||||
options: providerOptions,
|
||||
async getModel(sdk: any, modelID: string, options?: Record<string, any>) {
|
||||
async getModel(sdk: any, model: Model, options?: Record<string, any>) {
|
||||
let modelID = model.api.id
|
||||
// Skip region prefixing if model already has a cross-region inference profile prefix
|
||||
// Models from models.dev may already include prefixes like us., eu., global., etc.
|
||||
const crossRegionPrefixes = ["global.", "us.", "eu.", "jp.", "apac.", "au."]
|
||||
@@ -406,8 +414,8 @@ export namespace Provider {
|
||||
return fetch(input, { ...init, headers })
|
||||
},
|
||||
},
|
||||
async getModel(sdk: any, modelID: string) {
|
||||
const id = String(modelID).trim()
|
||||
async getModel(sdk: any, model: Model) {
|
||||
const id = String(model.api.id).trim()
|
||||
return sdk.languageModel(id)
|
||||
},
|
||||
}
|
||||
@@ -423,8 +431,8 @@ export namespace Provider {
|
||||
project,
|
||||
location,
|
||||
},
|
||||
async getModel(sdk: any, modelID) {
|
||||
const id = String(modelID).trim()
|
||||
async getModel(sdk: any, model: Model) {
|
||||
const id = String(model.api.id).trim()
|
||||
return sdk.languageModel(id)
|
||||
},
|
||||
}
|
||||
@@ -448,8 +456,8 @@ export namespace Provider {
|
||||
return {
|
||||
autoload: !!envServiceKey,
|
||||
options: envServiceKey ? { deploymentId, resourceGroup } : {},
|
||||
async getModel(sdk: any, modelID: string) {
|
||||
return sdk(modelID)
|
||||
async getModel(sdk: any, model: Model) {
|
||||
return sdk(model.api.id)
|
||||
},
|
||||
}
|
||||
},
|
||||
@@ -494,8 +502,8 @@ export namespace Provider {
|
||||
...(providerConfig?.options?.featureFlags || {}),
|
||||
},
|
||||
},
|
||||
async getModel(sdk: ReturnType<typeof createGitLab>, modelID: string) {
|
||||
return sdk.agenticChat(modelID, {
|
||||
async getModel(sdk: ReturnType<typeof createGitLab>, model: Model) {
|
||||
return sdk.agenticChat(model.api.id, {
|
||||
aiGatewayHeaders,
|
||||
featureFlags: {
|
||||
duo_agent_platform_agentic_chat: true,
|
||||
@@ -524,8 +532,8 @@ export namespace Provider {
|
||||
apiKey,
|
||||
baseURL: `https://api.cloudflare.com/client/v4/accounts/${accountId}/ai/v1`,
|
||||
},
|
||||
async getModel(sdk: any, modelID: string) {
|
||||
return sdk.languageModel(modelID)
|
||||
async getModel(sdk: any, model: Model) {
|
||||
return sdk.languageModel(model.api.id)
|
||||
},
|
||||
}
|
||||
},
|
||||
@@ -560,9 +568,9 @@ export namespace Provider {
|
||||
|
||||
return {
|
||||
autoload: true,
|
||||
async getModel(_sdk: any, modelID: string, _options?: Record<string, any>) {
|
||||
async getModel(_sdk: any, model: Model, _options?: Record<string, any>) {
|
||||
// Model IDs use Unified API format: provider/model (e.g., "anthropic/claude-sonnet-4-5")
|
||||
return aigateway(unified(modelID))
|
||||
return aigateway(unified(model.api.id))
|
||||
},
|
||||
options: {},
|
||||
}
|
||||
@@ -598,6 +606,7 @@ export namespace Provider {
|
||||
id: z.string(),
|
||||
url: z.string(),
|
||||
npm: z.string(),
|
||||
shape: z.enum(["responses", "completions"]).optional(),
|
||||
}),
|
||||
name: z.string(),
|
||||
family: z.string().optional(),
|
||||
@@ -686,6 +695,7 @@ export namespace Provider {
|
||||
id: model.id,
|
||||
url: model.provider?.api ?? provider.api!,
|
||||
npm: model.provider?.npm ?? provider.npm ?? "@ai-sdk/openai-compatible",
|
||||
shape: model.provider?.shape,
|
||||
},
|
||||
status: model.status ?? "active",
|
||||
headers: model.headers ?? {},
|
||||
@@ -836,6 +846,7 @@ export namespace Provider {
|
||||
existingModel?.api.npm ??
|
||||
modelsDev[providerID]?.npm ??
|
||||
"@ai-sdk/openai-compatible",
|
||||
shape: model.provider?.shape ?? existingModel?.api.shape,
|
||||
url: model.provider?.api ?? provider?.api ?? existingModel?.api.url ?? modelsDev[providerID]?.api,
|
||||
},
|
||||
status: model.status ?? existingModel?.status ?? "active",
|
||||
@@ -1177,7 +1188,7 @@ export namespace Provider {
|
||||
|
||||
try {
|
||||
const language = s.modelLoaders[model.providerID]
|
||||
? await s.modelLoaders[model.providerID](sdk, model.api.id, provider.options)
|
||||
? await s.modelLoaders[model.providerID](sdk, model, provider.options)
|
||||
: sdk.languageModel(model.api.id)
|
||||
s.models.set(key, language)
|
||||
return language
|
||||
|
||||
61
packages/ui/src/components/scroll-view.css
Normal file
61
packages/ui/src/components/scroll-view.css
Normal file
@@ -0,0 +1,61 @@
|
||||
.scroll-view {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.scroll-view__viewport {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
overflow-y: auto;
|
||||
scrollbar-width: none;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.scroll-view__viewport::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.scroll-view__thumb {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: 0;
|
||||
width: 16px;
|
||||
transition: opacity 200ms ease;
|
||||
cursor: default;
|
||||
user-select: none;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.scroll-view__thumb::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
right: 4px;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
width: 6px;
|
||||
border-radius: 9999px;
|
||||
background-color: var(--border-weak-base);
|
||||
backdrop-filter: blur(4px);
|
||||
transition: background-color 150ms ease;
|
||||
}
|
||||
|
||||
.scroll-view__thumb:hover::after,
|
||||
.scroll-view__thumb[data-dragging="true"]::after {
|
||||
background-color: var(--border-strong-base);
|
||||
}
|
||||
|
||||
.dark .scroll-view__thumb::after,
|
||||
[data-theme="dark"] .scroll-view__thumb::after {
|
||||
background-color: var(--border-weak-base);
|
||||
}
|
||||
|
||||
.dark .scroll-view__thumb:hover::after,
|
||||
[data-theme="dark"] .scroll-view__thumb:hover::after,
|
||||
.dark .scroll-view__thumb[data-dragging="true"]::after,
|
||||
[data-theme="dark"] .scroll-view__thumb[data-dragging="true"]::after {
|
||||
background-color: var(--border-strong-base);
|
||||
}
|
||||
|
||||
.scroll-view__thumb[data-visible="true"] {
|
||||
opacity: 1;
|
||||
}
|
||||
217
packages/ui/src/components/scroll-view.tsx
Normal file
217
packages/ui/src/components/scroll-view.tsx
Normal file
@@ -0,0 +1,217 @@
|
||||
import { createSignal, onCleanup, onMount, splitProps, type ComponentProps, Show, mergeProps } from "solid-js"
|
||||
|
||||
export interface ScrollViewProps extends ComponentProps<"div"> {
|
||||
viewportRef?: (el: HTMLDivElement) => void
|
||||
orientation?: "vertical" | "horizontal" // currently only vertical is fully implemented for thumb
|
||||
}
|
||||
|
||||
export function ScrollView(props: ScrollViewProps) {
|
||||
const merged = mergeProps({ orientation: "vertical" }, props)
|
||||
const [local, events, rest] = splitProps(
|
||||
merged,
|
||||
["class", "children", "viewportRef", "orientation", "style"],
|
||||
[
|
||||
"onScroll",
|
||||
"onWheel",
|
||||
"onTouchStart",
|
||||
"onTouchMove",
|
||||
"onTouchEnd",
|
||||
"onTouchCancel",
|
||||
"onPointerDown",
|
||||
"onClick",
|
||||
"onKeyDown",
|
||||
],
|
||||
)
|
||||
|
||||
let rootRef!: HTMLDivElement
|
||||
let viewportRef!: HTMLDivElement
|
||||
let thumbRef!: HTMLDivElement
|
||||
|
||||
const [isHovered, setIsHovered] = createSignal(false)
|
||||
const [isDragging, setIsDragging] = createSignal(false)
|
||||
|
||||
const [thumbHeight, setThumbHeight] = createSignal(0)
|
||||
const [thumbTop, setThumbTop] = createSignal(0)
|
||||
const [showThumb, setShowThumb] = createSignal(false)
|
||||
|
||||
const updateThumb = () => {
|
||||
if (!viewportRef) return
|
||||
const { scrollTop, scrollHeight, clientHeight } = viewportRef
|
||||
|
||||
if (scrollHeight <= clientHeight || scrollHeight === 0) {
|
||||
setShowThumb(false)
|
||||
return
|
||||
}
|
||||
|
||||
setShowThumb(true)
|
||||
const trackPadding = 8
|
||||
const trackHeight = clientHeight - trackPadding * 2
|
||||
|
||||
const minThumbHeight = 32
|
||||
// Calculate raw thumb height based on ratio
|
||||
let height = (clientHeight / scrollHeight) * trackHeight
|
||||
height = Math.max(height, minThumbHeight)
|
||||
|
||||
const maxScrollTop = scrollHeight - clientHeight
|
||||
const maxThumbTop = trackHeight - height
|
||||
|
||||
const top = maxScrollTop > 0 ? (scrollTop / maxScrollTop) * maxThumbTop : 0
|
||||
|
||||
// Ensure thumb stays within bounds (shouldn't be necessary due to math above, but good for safety)
|
||||
const boundedTop = trackPadding + Math.max(0, Math.min(top, maxThumbTop))
|
||||
|
||||
setThumbHeight(height)
|
||||
setThumbTop(boundedTop)
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
if (local.viewportRef) {
|
||||
local.viewportRef(viewportRef)
|
||||
}
|
||||
|
||||
const observer = new ResizeObserver(() => {
|
||||
updateThumb()
|
||||
})
|
||||
|
||||
observer.observe(viewportRef)
|
||||
// Also observe the first child if possible to catch content changes
|
||||
if (viewportRef.firstElementChild) {
|
||||
observer.observe(viewportRef.firstElementChild)
|
||||
}
|
||||
|
||||
onCleanup(() => {
|
||||
observer.disconnect()
|
||||
})
|
||||
|
||||
updateThumb()
|
||||
})
|
||||
|
||||
let startY = 0
|
||||
let startScrollTop = 0
|
||||
|
||||
const onThumbPointerDown = (e: PointerEvent) => {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
setIsDragging(true)
|
||||
startY = e.clientY
|
||||
startScrollTop = viewportRef.scrollTop
|
||||
|
||||
thumbRef.setPointerCapture(e.pointerId)
|
||||
|
||||
const onPointerMove = (e: PointerEvent) => {
|
||||
const deltaY = e.clientY - startY
|
||||
const { scrollHeight, clientHeight } = viewportRef
|
||||
const maxScrollTop = scrollHeight - clientHeight
|
||||
const maxThumbTop = clientHeight - thumbHeight()
|
||||
|
||||
if (maxThumbTop > 0) {
|
||||
const scrollDelta = deltaY * (maxScrollTop / maxThumbTop)
|
||||
viewportRef.scrollTop = startScrollTop + scrollDelta
|
||||
}
|
||||
}
|
||||
|
||||
const onPointerUp = (e: PointerEvent) => {
|
||||
setIsDragging(false)
|
||||
thumbRef.releasePointerCapture(e.pointerId)
|
||||
thumbRef.removeEventListener("pointermove", onPointerMove)
|
||||
thumbRef.removeEventListener("pointerup", onPointerUp)
|
||||
}
|
||||
|
||||
thumbRef.addEventListener("pointermove", onPointerMove)
|
||||
thumbRef.addEventListener("pointerup", onPointerUp)
|
||||
}
|
||||
|
||||
// Keybinds implementation
|
||||
// We ensure the viewport has a tabindex so it can receive focus
|
||||
// We can also explicitly catch PageUp/Down if we want smooth scroll or specific behavior,
|
||||
// but native usually handles this perfectly. Let's explicitly ensure it behaves well.
|
||||
const onKeyDown = (e: KeyboardEvent) => {
|
||||
// If user is focused on an input inside the scroll view, don't hijack keys
|
||||
if (document.activeElement && ["INPUT", "TEXTAREA", "SELECT"].includes(document.activeElement.tagName)) {
|
||||
return
|
||||
}
|
||||
|
||||
const scrollAmount = viewportRef.clientHeight * 0.8
|
||||
const lineAmount = 40
|
||||
|
||||
switch (e.key) {
|
||||
case "PageDown":
|
||||
e.preventDefault()
|
||||
viewportRef.scrollBy({ top: scrollAmount, behavior: "smooth" })
|
||||
break
|
||||
case "PageUp":
|
||||
e.preventDefault()
|
||||
viewportRef.scrollBy({ top: -scrollAmount, behavior: "smooth" })
|
||||
break
|
||||
case "Home":
|
||||
e.preventDefault()
|
||||
viewportRef.scrollTo({ top: 0, behavior: "smooth" })
|
||||
break
|
||||
case "End":
|
||||
e.preventDefault()
|
||||
viewportRef.scrollTo({ top: viewportRef.scrollHeight, behavior: "smooth" })
|
||||
break
|
||||
case "ArrowUp":
|
||||
e.preventDefault()
|
||||
viewportRef.scrollBy({ top: -lineAmount, behavior: "smooth" })
|
||||
break
|
||||
case "ArrowDown":
|
||||
e.preventDefault()
|
||||
viewportRef.scrollBy({ top: lineAmount, behavior: "smooth" })
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={rootRef}
|
||||
class={`scroll-view ${local.class || ""}`}
|
||||
style={local.style}
|
||||
onPointerEnter={() => setIsHovered(true)}
|
||||
onPointerLeave={() => setIsHovered(false)}
|
||||
{...rest}
|
||||
>
|
||||
{/* Viewport */}
|
||||
<div
|
||||
ref={viewportRef}
|
||||
class="scroll-view__viewport"
|
||||
onScroll={(e) => {
|
||||
updateThumb()
|
||||
if (typeof events.onScroll === "function") events.onScroll(e as any)
|
||||
}}
|
||||
onWheel={events.onWheel as any}
|
||||
onTouchStart={events.onTouchStart as any}
|
||||
onTouchMove={events.onTouchMove as any}
|
||||
onTouchEnd={events.onTouchEnd as any}
|
||||
onTouchCancel={events.onTouchCancel as any}
|
||||
onPointerDown={events.onPointerDown as any}
|
||||
onClick={events.onClick as any}
|
||||
tabIndex={0}
|
||||
role="region"
|
||||
aria-label="scrollable content"
|
||||
onKeyDown={(e) => {
|
||||
onKeyDown(e)
|
||||
if (typeof events.onKeyDown === "function") events.onKeyDown(e as any)
|
||||
}}
|
||||
>
|
||||
{local.children}
|
||||
</div>
|
||||
|
||||
{/* Thumb Overlay */}
|
||||
<Show when={showThumb()}>
|
||||
<div
|
||||
ref={thumbRef}
|
||||
onPointerDown={onThumbPointerDown}
|
||||
class="scroll-view__thumb"
|
||||
data-visible={isHovered() || isDragging()}
|
||||
data-dragging={isDragging()}
|
||||
style={{
|
||||
height: `${thumbHeight()}px`,
|
||||
transform: `translateY(${thumbTop()}px)`,
|
||||
"z-index": 100, // ensure it displays over content
|
||||
}}
|
||||
/>
|
||||
</Show>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -12,6 +12,7 @@
|
||||
|
||||
[data-slot="session-review-container"] {
|
||||
flex: 1 1 auto;
|
||||
padding-right: 4px;
|
||||
}
|
||||
|
||||
[data-slot="session-review-header"] {
|
||||
@@ -40,7 +41,6 @@
|
||||
display: flex;
|
||||
align-items: center;
|
||||
column-gap: 12px;
|
||||
padding-right: 1px;
|
||||
}
|
||||
|
||||
[data-slot="session-review-actions"] [data-component="radio-group"] {
|
||||
|
||||
@@ -7,6 +7,7 @@ import { Icon } from "./icon"
|
||||
import { LineComment, LineCommentEditor } from "./line-comment"
|
||||
import { StickyAccordionHeader } from "./sticky-accordion-header"
|
||||
import { Tooltip } from "./tooltip"
|
||||
import { ScrollView } from "./scroll-view"
|
||||
import { useDiffComponent } from "../context/diff"
|
||||
import { useI18n } from "../context/i18n"
|
||||
import { getDirectory, getFilename } from "@opencode-ai/util/path"
|
||||
@@ -188,8 +189,10 @@ export const SessionReview = (props: SessionReviewProps) => {
|
||||
const [opened, setOpened] = createSignal<SessionReviewFocus | null>(null)
|
||||
|
||||
const open = () => props.open ?? store.open
|
||||
const files = createMemo(() => props.diffs.map((d) => d.file))
|
||||
const diffs = createMemo(() => new Map(props.diffs.map((d) => [d.file, d] as const)))
|
||||
const diffStyle = () => props.diffStyle ?? (props.split ? "split" : "unified")
|
||||
const hasDiffs = () => props.diffs.length > 0
|
||||
const hasDiffs = () => files().length > 0
|
||||
|
||||
const handleChange = (open: string[]) => {
|
||||
props.onOpenChange?.(open)
|
||||
@@ -198,7 +201,7 @@ export const SessionReview = (props: SessionReviewProps) => {
|
||||
}
|
||||
|
||||
const handleExpandOrCollapseAll = () => {
|
||||
const next = open().length > 0 ? [] : props.diffs.map((d) => d.file)
|
||||
const next = open().length > 0 ? [] : files()
|
||||
handleChange(next)
|
||||
}
|
||||
|
||||
@@ -274,13 +277,13 @@ export const SessionReview = (props: SessionReviewProps) => {
|
||||
})
|
||||
|
||||
return (
|
||||
<div
|
||||
<ScrollView
|
||||
data-component="session-review"
|
||||
ref={(el) => {
|
||||
viewportRef={(el) => {
|
||||
scroll = el
|
||||
props.scrollRef?.(el)
|
||||
}}
|
||||
onScroll={props.onScroll}
|
||||
onScroll={props.onScroll as any}
|
||||
classList={{
|
||||
...(props.classList ?? {}),
|
||||
[props.classes?.root ?? ""]: !!props.classes?.root,
|
||||
@@ -321,51 +324,54 @@ export const SessionReview = (props: SessionReviewProps) => {
|
||||
<div data-slot="session-review-container" class={props.classes?.container}>
|
||||
<Show when={hasDiffs()} fallback={props.empty}>
|
||||
<Accordion multiple value={open()} onChange={handleChange}>
|
||||
<For each={props.diffs}>
|
||||
{(diff) => {
|
||||
<For each={files()}>
|
||||
{(file) => {
|
||||
let wrapper: HTMLDivElement | undefined
|
||||
|
||||
const expanded = createMemo(() => open().includes(diff.file))
|
||||
const diff = createMemo(() => diffs().get(file))
|
||||
const item = () => diff()!
|
||||
|
||||
const expanded = createMemo(() => open().includes(file))
|
||||
const [force, setForce] = createSignal(false)
|
||||
|
||||
const comments = createMemo(() => (props.comments ?? []).filter((c) => c.file === diff.file))
|
||||
const comments = createMemo(() => (props.comments ?? []).filter((c) => c.file === file))
|
||||
const commentedLines = createMemo(() => comments().map((c) => c.selection))
|
||||
|
||||
const beforeText = () => (typeof diff.before === "string" ? diff.before : "")
|
||||
const afterText = () => (typeof diff.after === "string" ? diff.after : "")
|
||||
const changedLines = () => diff.additions + diff.deletions
|
||||
const beforeText = () => (typeof item().before === "string" ? item().before : "")
|
||||
const afterText = () => (typeof item().after === "string" ? item().after : "")
|
||||
const changedLines = () => item().additions + item().deletions
|
||||
|
||||
const tooLarge = createMemo(() => {
|
||||
if (!expanded()) return false
|
||||
if (force()) return false
|
||||
if (isImageFile(diff.file)) return false
|
||||
if (isImageFile(file)) return false
|
||||
return changedLines() > MAX_DIFF_CHANGED_LINES
|
||||
})
|
||||
|
||||
const isAdded = () => diff.status === "added" || (beforeText().length === 0 && afterText().length > 0)
|
||||
const isAdded = () => item().status === "added" || (beforeText().length === 0 && afterText().length > 0)
|
||||
const isDeleted = () =>
|
||||
diff.status === "deleted" || (afterText().length === 0 && beforeText().length > 0)
|
||||
const isImage = () => isImageFile(diff.file)
|
||||
const isAudio = () => isAudioFile(diff.file)
|
||||
item().status === "deleted" || (afterText().length === 0 && beforeText().length > 0)
|
||||
const isImage = () => isImageFile(file)
|
||||
const isAudio = () => isAudioFile(file)
|
||||
|
||||
const diffImageSrc = dataUrlFromValue(diff.after) ?? dataUrlFromValue(diff.before)
|
||||
const [imageSrc, setImageSrc] = createSignal<string | undefined>(diffImageSrc)
|
||||
const diffImageSrc = createMemo(() => dataUrlFromValue(item().after) ?? dataUrlFromValue(item().before))
|
||||
const [imageSrc, setImageSrc] = createSignal<string | undefined>(diffImageSrc())
|
||||
const [imageStatus, setImageStatus] = createSignal<"idle" | "loading" | "error">("idle")
|
||||
|
||||
const diffAudioSrc = dataUrlFromValue(diff.after) ?? dataUrlFromValue(diff.before)
|
||||
const [audioSrc, setAudioSrc] = createSignal<string | undefined>(diffAudioSrc)
|
||||
const diffAudioSrc = createMemo(() => dataUrlFromValue(item().after) ?? dataUrlFromValue(item().before))
|
||||
const [audioSrc, setAudioSrc] = createSignal<string | undefined>(diffAudioSrc())
|
||||
const [audioStatus, setAudioStatus] = createSignal<"idle" | "loading" | "error">("idle")
|
||||
const [audioMime, setAudioMime] = createSignal<string | undefined>(undefined)
|
||||
|
||||
const selectedLines = createMemo(() => {
|
||||
const current = selection()
|
||||
if (!current || current.file !== diff.file) return null
|
||||
if (!current || current.file !== file) return null
|
||||
return current.range
|
||||
})
|
||||
|
||||
const draftRange = createMemo(() => {
|
||||
const current = commenting()
|
||||
if (!current || current.file !== diff.file) return null
|
||||
if (!current || current.file !== file) return null
|
||||
return current.range
|
||||
})
|
||||
|
||||
@@ -416,6 +422,21 @@ export const SessionReview = (props: SessionReviewProps) => {
|
||||
requestAnimationFrame(updateAnchors)
|
||||
}
|
||||
|
||||
createEffect(() => {
|
||||
if (!isImage()) return
|
||||
const src = diffImageSrc()
|
||||
setImageSrc(src)
|
||||
setImageStatus("idle")
|
||||
})
|
||||
|
||||
createEffect(() => {
|
||||
if (!isAudio()) return
|
||||
const src = diffAudioSrc()
|
||||
setAudioSrc(src)
|
||||
setAudioStatus("idle")
|
||||
setAudioMime(undefined)
|
||||
})
|
||||
|
||||
createEffect(() => {
|
||||
comments()
|
||||
scheduleAnchors()
|
||||
@@ -429,7 +450,7 @@ export const SessionReview = (props: SessionReviewProps) => {
|
||||
})
|
||||
|
||||
createEffect(() => {
|
||||
if (!open().includes(diff.file)) return
|
||||
if (!open().includes(file)) return
|
||||
if (!isImage()) return
|
||||
if (imageSrc()) return
|
||||
if (imageStatus() !== "idle") return
|
||||
@@ -439,7 +460,7 @@ export const SessionReview = (props: SessionReviewProps) => {
|
||||
if (!reader) return
|
||||
|
||||
setImageStatus("loading")
|
||||
reader(diff.file)
|
||||
reader(file)
|
||||
.then((result) => {
|
||||
const src = dataUrl(result)
|
||||
if (!src) {
|
||||
@@ -455,7 +476,7 @@ export const SessionReview = (props: SessionReviewProps) => {
|
||||
})
|
||||
|
||||
createEffect(() => {
|
||||
if (!open().includes(diff.file)) return
|
||||
if (!open().includes(file)) return
|
||||
if (!isAudio()) return
|
||||
if (audioSrc()) return
|
||||
if (audioStatus() !== "idle") return
|
||||
@@ -464,7 +485,7 @@ export const SessionReview = (props: SessionReviewProps) => {
|
||||
if (!reader) return
|
||||
|
||||
setAudioStatus("loading")
|
||||
reader(diff.file)
|
||||
reader(file)
|
||||
.then((result) => {
|
||||
const src = dataUrl(result)
|
||||
if (!src) {
|
||||
@@ -488,7 +509,7 @@ export const SessionReview = (props: SessionReviewProps) => {
|
||||
return
|
||||
}
|
||||
|
||||
setSelection({ file: diff.file, range })
|
||||
setSelection({ file, range })
|
||||
}
|
||||
|
||||
const handleLineSelectionEnd = (range: SelectedLineRange | null) => {
|
||||
@@ -499,8 +520,8 @@ export const SessionReview = (props: SessionReviewProps) => {
|
||||
return
|
||||
}
|
||||
|
||||
setSelection({ file: diff.file, range })
|
||||
setCommenting({ file: diff.file, range })
|
||||
setSelection({ file, range })
|
||||
setCommenting({ file, range })
|
||||
}
|
||||
|
||||
const openComment = (comment: SessionReviewComment) => {
|
||||
@@ -516,22 +537,22 @@ export const SessionReview = (props: SessionReviewProps) => {
|
||||
|
||||
return (
|
||||
<Accordion.Item
|
||||
value={diff.file}
|
||||
id={diffId(diff.file)}
|
||||
data-file={diff.file}
|
||||
value={file}
|
||||
id={diffId(file)}
|
||||
data-file={file}
|
||||
data-slot="session-review-accordion-item"
|
||||
data-selected={props.focusedFile === diff.file ? "" : undefined}
|
||||
data-selected={props.focusedFile === file ? "" : undefined}
|
||||
>
|
||||
<StickyAccordionHeader>
|
||||
<Accordion.Trigger>
|
||||
<div data-slot="session-review-trigger-content">
|
||||
<div data-slot="session-review-file-info">
|
||||
<FileIcon node={{ path: diff.file, type: "file" }} />
|
||||
<FileIcon node={{ path: file, type: "file" }} />
|
||||
<div data-slot="session-review-file-name-container">
|
||||
<Show when={diff.file.includes("/")}>
|
||||
<span data-slot="session-review-directory">{`\u202A${getDirectory(diff.file)}\u202C`}</span>
|
||||
<Show when={file.includes("/")}>
|
||||
<span data-slot="session-review-directory">{`\u202A${getDirectory(file)}\u202C`}</span>
|
||||
</Show>
|
||||
<span data-slot="session-review-filename">{getFilename(diff.file)}</span>
|
||||
<span data-slot="session-review-filename">{getFilename(file)}</span>
|
||||
<Show when={props.onViewFile}>
|
||||
<Tooltip value="Open file" placement="top" gutter={4}>
|
||||
<button
|
||||
@@ -540,7 +561,7 @@ export const SessionReview = (props: SessionReviewProps) => {
|
||||
aria-label="Open file"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
props.onViewFile?.(diff.file)
|
||||
props.onViewFile?.(file)
|
||||
}}
|
||||
>
|
||||
<Icon name="open-file" size="small" />
|
||||
@@ -556,7 +577,7 @@ export const SessionReview = (props: SessionReviewProps) => {
|
||||
<span data-slot="session-review-change" data-type="added">
|
||||
{i18n.t("ui.sessionReview.change.added")}
|
||||
</span>
|
||||
<DiffChanges changes={diff} />
|
||||
<DiffChanges changes={item()} />
|
||||
</div>
|
||||
</Match>
|
||||
<Match when={isDeleted()}>
|
||||
@@ -570,7 +591,7 @@ export const SessionReview = (props: SessionReviewProps) => {
|
||||
</span>
|
||||
</Match>
|
||||
<Match when={true}>
|
||||
<DiffChanges changes={diff} />
|
||||
<DiffChanges changes={item()} />
|
||||
</Match>
|
||||
</Switch>
|
||||
<span data-slot="session-review-diff-chevron">
|
||||
@@ -585,7 +606,7 @@ export const SessionReview = (props: SessionReviewProps) => {
|
||||
data-slot="session-review-diff-wrapper"
|
||||
ref={(el) => {
|
||||
wrapper = el
|
||||
anchors.set(diff.file, el)
|
||||
anchors.set(file, el)
|
||||
scheduleAnchors()
|
||||
}}
|
||||
>
|
||||
@@ -593,7 +614,7 @@ export const SessionReview = (props: SessionReviewProps) => {
|
||||
<Switch>
|
||||
<Match when={isImage() && imageSrc()}>
|
||||
<div data-slot="session-review-image-container">
|
||||
<img data-slot="session-review-image" src={imageSrc()} alt={diff.file} />
|
||||
<img data-slot="session-review-image" src={imageSrc()} alt={file} />
|
||||
</div>
|
||||
</Match>
|
||||
<Match when={isImage() && isDeleted()}>
|
||||
@@ -633,7 +654,7 @@ export const SessionReview = (props: SessionReviewProps) => {
|
||||
<Match when={!isImage()}>
|
||||
<Dynamic
|
||||
component={diffComponent}
|
||||
preloadedDiff={diff.preloaded}
|
||||
preloadedDiff={item().preloaded}
|
||||
diffStyle={diffStyle()}
|
||||
onRendered={() => {
|
||||
props.onDiffRendered?.()
|
||||
@@ -645,12 +666,12 @@ export const SessionReview = (props: SessionReviewProps) => {
|
||||
selectedLines={selectedLines()}
|
||||
commentedLines={commentedLines()}
|
||||
before={{
|
||||
name: diff.file!,
|
||||
contents: typeof diff.before === "string" ? diff.before : "",
|
||||
name: file,
|
||||
contents: typeof item().before === "string" ? item().before : "",
|
||||
}}
|
||||
after={{
|
||||
name: diff.file!,
|
||||
contents: typeof diff.after === "string" ? diff.after : "",
|
||||
name: file,
|
||||
contents: typeof item().after === "string" ? item().after : "",
|
||||
}}
|
||||
/>
|
||||
</Match>
|
||||
@@ -688,10 +709,10 @@ export const SessionReview = (props: SessionReviewProps) => {
|
||||
onCancel={() => setCommenting(null)}
|
||||
onSubmit={(comment) => {
|
||||
props.onLineComment?.({
|
||||
file: diff.file,
|
||||
file,
|
||||
selection: range(),
|
||||
comment,
|
||||
preview: selectionPreview(diff, range()),
|
||||
preview: selectionPreview(item(), range()),
|
||||
})
|
||||
setCommenting(null)
|
||||
}}
|
||||
@@ -709,6 +730,6 @@ export const SessionReview = (props: SessionReviewProps) => {
|
||||
</Accordion>
|
||||
</Show>
|
||||
</div>
|
||||
</div>
|
||||
</ScrollView>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -37,10 +37,11 @@ function target(container: HTMLElement): Target | undefined {
|
||||
|
||||
const review = container.closest("[data-component='session-review']")
|
||||
if (review instanceof HTMLElement) {
|
||||
const root = scrollRoot(container) ?? review
|
||||
const content = review.querySelector("[data-slot='session-review-container']")
|
||||
return {
|
||||
key: review,
|
||||
root: review,
|
||||
root,
|
||||
content: content instanceof HTMLElement ? content : undefined,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -44,6 +44,7 @@
|
||||
@import "../components/select.css" layer(components);
|
||||
@import "../components/spinner.css" layer(components);
|
||||
@import "../components/switch.css" layer(components);
|
||||
@import "../components/scroll-view.css" layer(components);
|
||||
@import "../components/session-review.css" layer(components);
|
||||
@import "../components/session-turn.css" layer(components);
|
||||
@import "../components/sticky-accordion-header.css" layer(components);
|
||||
|
||||
@@ -8,34 +8,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
@utility session-scroller {
|
||||
&::-webkit-scrollbar {
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-thumb {
|
||||
background: var(--border-weak-base);
|
||||
border-radius: 5px;
|
||||
border: 3px solid transparent;
|
||||
background-clip: padding-box;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-thumb:hover {
|
||||
background: var(--border-weak-base);
|
||||
}
|
||||
|
||||
& {
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: var(--border-weak-base) transparent;
|
||||
}
|
||||
}
|
||||
|
||||
@utility badge-mask {
|
||||
-webkit-mask-image: radial-gradient(circle 5px at calc(100% - 4px) 4px, transparent 5px, black 5.5px);
|
||||
mask-image: radial-gradient(circle 5px at calc(100% - 4px) 4px, transparent 5px, black 5.5px);
|
||||
|
||||
Reference in New Issue
Block a user