mirror of
https://github.com/anomalyco/opencode.git
synced 2026-07-30 15:11:11 -04:00
Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 541b9b5c78 |
@@ -94,7 +94,7 @@ test("shows a pending permission dock", async ({ page }) => {
|
||||
await expect(permission).toBeVisible()
|
||||
await expect(permission.getByText("git status")).toBeVisible()
|
||||
await expect(permission.getByText("git diff")).toBeVisible()
|
||||
await expect(permission.locator('[data-slot="permission-footer-actions"] button')).toHaveCount(3)
|
||||
await expect(permission.locator('[data-slot="permission-footer-actions"] button')).toHaveCount(2)
|
||||
await expect(page.locator('[data-component="session-composer"]')).toHaveCount(0)
|
||||
|
||||
const reply = page.waitForRequest((request) => request.method() === "POST")
|
||||
|
||||
@@ -527,8 +527,6 @@ type SessionHeaderV2ActionsState = {
|
||||
}
|
||||
|
||||
function SessionHeaderV2Actions(props: { state: SessionHeaderV2ActionsState }) {
|
||||
const language = useLanguage()
|
||||
|
||||
return (
|
||||
<div class="flex items-center gap-2">
|
||||
<Show when={props.state.statusVisible}>
|
||||
|
||||
@@ -711,9 +711,9 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
|
||||
return
|
||||
}
|
||||
setStore("fileTree", "tab", tab)
|
||||
},
|
||||
open() {
|
||||
if (!store.fileTree) {
|
||||
},
|
||||
open() {
|
||||
if (!store.fileTree) {
|
||||
setStore("fileTree", { opened: true, width: DEFAULT_FILE_TREE_WIDTH, tab: "changes" })
|
||||
return
|
||||
}
|
||||
@@ -726,12 +726,13 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
|
||||
}
|
||||
setStore("fileTree", "opened", false)
|
||||
},
|
||||
toggle() {
|
||||
if (!store.fileTree) {
|
||||
toggle() {
|
||||
const next = !(store.fileTree?.opened ?? true)
|
||||
if (!store.fileTree) {
|
||||
setStore("fileTree", { opened: true, width: DEFAULT_FILE_TREE_WIDTH, tab: "changes" })
|
||||
return
|
||||
}
|
||||
setStore("fileTree", "opened", (x) => !x)
|
||||
setStore("fileTree", "opened", next)
|
||||
},
|
||||
resize(width: number) {
|
||||
if (!store.fileTree) {
|
||||
|
||||
@@ -20,7 +20,6 @@ const appLocales = [
|
||||
"zht",
|
||||
] as const
|
||||
const desktopLocales = appLocales.filter((locale) => locale !== "th" && locale !== "tr")
|
||||
|
||||
const domains = [
|
||||
{
|
||||
name: "app",
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
Match,
|
||||
Switch,
|
||||
createMemo,
|
||||
createResource,
|
||||
createEffect,
|
||||
createComputed,
|
||||
createSignal,
|
||||
@@ -50,7 +51,8 @@ import { useLayout } from "@/context/layout"
|
||||
import { ModelsProvider } from "@/context/models"
|
||||
import { useNotification } from "@/context/notification"
|
||||
import { PromptProvider, usePrompt } from "@/context/prompt"
|
||||
import { usePlatform } from "@/context/platform"
|
||||
import { browserPaneAvailable, createBrowserPaneBinding, usePlatform } from "@/context/platform"
|
||||
import type { BrowserPaneRegistration } from "@/context/platform"
|
||||
import { SDKProvider, useSDK } from "@/context/sdk"
|
||||
import { useServerSDK } from "@/context/server-sdk"
|
||||
import { ServerConnection, serverName, useServer } from "@/context/server"
|
||||
@@ -103,6 +105,7 @@ import { legacySessionHref, requireServerKey, sessionHref } from "@/utils/sessio
|
||||
import { useUsageExceededDialogs } from "./session/usage-exceeded-dialogs"
|
||||
import { createSessionOwnership } from "./session/session-ownership"
|
||||
import { createSessionLineage } from "./session/session-lineage"
|
||||
import { SessionBrowserPane } from "./session/browser-pane"
|
||||
|
||||
type FollowupItem = FollowupDraft & { id: string }
|
||||
type FollowupEdit = Pick<FollowupItem, "id" | "prompt" | "context">
|
||||
@@ -352,6 +355,7 @@ function SessionPanelFrame(props: ParentProps<{ newLayout: boolean; raised?: boo
|
||||
|
||||
export default function Page() {
|
||||
const serverSync = useServerSync()
|
||||
const server = useServer()
|
||||
const layout = useLayout()
|
||||
const local = useLocal()
|
||||
const file = useFile()
|
||||
@@ -447,12 +451,87 @@ export default function Page() {
|
||||
|
||||
const isDesktop = createMediaQuery("(min-width: 768px)")
|
||||
const size = createSizing()
|
||||
const [serverProtocol] = createResource(
|
||||
() => serverSDK().protocol,
|
||||
(protocol) => protocol,
|
||||
)
|
||||
const browserAvailable = createMemo(() =>
|
||||
browserPaneAvailable({
|
||||
platform: !!platform.browserPane,
|
||||
sessionID: params.id,
|
||||
protocol: serverProtocol(),
|
||||
}),
|
||||
)
|
||||
const browserServer = createMemo(
|
||||
() => {
|
||||
if (!browserAvailable()) return undefined
|
||||
const routeServer = serverSDK().server
|
||||
const serverKey = ServerConnection.key(routeServer)
|
||||
const connection = server.list.find((item) => ServerConnection.key(item) === serverKey) ?? routeServer
|
||||
return {
|
||||
endpoint: {
|
||||
url: connection.http.url,
|
||||
username: connection.http.username,
|
||||
password: connection.http.password,
|
||||
},
|
||||
}
|
||||
},
|
||||
undefined,
|
||||
{
|
||||
equals: (left, right) => {
|
||||
if (!left || !right) return left === right
|
||||
return (
|
||||
left.endpoint.url === right.endpoint.url &&
|
||||
left.endpoint.username === right.endpoint.username &&
|
||||
left.endpoint.password === right.endpoint.password
|
||||
)
|
||||
},
|
||||
},
|
||||
)
|
||||
const browserBinding = createMemo(() => {
|
||||
if (!params.id) return undefined
|
||||
const target = browserServer()
|
||||
if (!target) return undefined
|
||||
return createBrowserPaneBinding({ sessionID: params.id, endpoint: target.endpoint })
|
||||
})
|
||||
const [browserPane, setBrowserPane] = createStore<{
|
||||
registration?: BrowserPaneRegistration
|
||||
opened: boolean
|
||||
}>({ opened: false })
|
||||
createEffect(() => {
|
||||
const binding = browserBinding()
|
||||
const browser = platform.browserPane
|
||||
if (!binding || !browser) {
|
||||
setBrowserPane({ registration: undefined, opened: false })
|
||||
return
|
||||
}
|
||||
const registration = browser.register(binding, () => {
|
||||
view().reviewPanel.close()
|
||||
layout.fileTree.close()
|
||||
setBrowserPane("opened", true)
|
||||
})
|
||||
setBrowserPane("registration", registration)
|
||||
onCleanup(() => {
|
||||
setBrowserPane({ registration: undefined, opened: false })
|
||||
registration.close()
|
||||
})
|
||||
})
|
||||
createEffect(() => {
|
||||
if (!browserPane.opened) return
|
||||
if (!view().reviewPanel.opened() && !layout.fileTree.opened()) return
|
||||
setBrowserPane("opened", false)
|
||||
})
|
||||
const desktopBrowserBinding = createMemo(() => {
|
||||
if (!isDesktop() || !browserAvailable() || !browserPane.opened) return undefined
|
||||
return browserPane.registration
|
||||
})
|
||||
const desktopBrowserOpen = createMemo(() => !!desktopBrowserBinding())
|
||||
const desktopReviewOpen = createMemo(() => isDesktop() && view().reviewPanel.opened())
|
||||
const desktopV2ReviewOpen = createMemo(() => newSessionDesign() && desktopReviewOpen() && !!params.id)
|
||||
const terminalOpen = createMemo(() => view().terminal.opened())
|
||||
const desktopTerminalOpen = createMemo(() => isDesktop() && terminalOpen())
|
||||
const desktopInlineTerminalOnlyOpen = createMemo(
|
||||
() => newSessionDesign() && desktopTerminalOpen() && !desktopV2ReviewOpen(),
|
||||
() => newSessionDesign() && desktopTerminalOpen() && !desktopV2ReviewOpen() && !desktopBrowserOpen(),
|
||||
)
|
||||
const desktopFileTreeOpen = createMemo(
|
||||
() =>
|
||||
@@ -463,7 +542,9 @@ export default function Page() {
|
||||
}),
|
||||
)
|
||||
const desktopSessionResizeOpen = createMemo(() =>
|
||||
newSessionDesign() ? desktopV2ReviewOpen() || desktopTerminalOpen() : desktopReviewOpen(),
|
||||
newSessionDesign()
|
||||
? desktopV2ReviewOpen() || desktopBrowserOpen() || desktopTerminalOpen()
|
||||
: desktopReviewOpen() || desktopBrowserOpen(),
|
||||
)
|
||||
const desktopSidePanelOpen = createMemo(() => desktopSessionResizeOpen() || desktopFileTreeOpen())
|
||||
let panelRow: HTMLDivElement | undefined
|
||||
@@ -505,6 +586,7 @@ export default function Page() {
|
||||
const desktopV2PanelLayout = createMemo(() =>
|
||||
sessionPanelLayout({
|
||||
review: desktopV2ReviewOpen(),
|
||||
browser: desktopBrowserOpen(),
|
||||
terminal: desktopTerminalOpen(),
|
||||
files: desktopFileTreeOpen(),
|
||||
}),
|
||||
@@ -2298,27 +2380,42 @@ export default function Page() {
|
||||
</div>
|
||||
|
||||
<Show when={!newSessionDesign() && desktopSidePanelOpen()}>
|
||||
<Suspense>
|
||||
<SessionSidePanel
|
||||
canReview={canReview}
|
||||
diffs={reviewDiffs}
|
||||
diffsReady={reviewReady}
|
||||
empty={reviewEmptyText}
|
||||
hasReview={hasReview}
|
||||
reviewHasFocusableContent={hasReview}
|
||||
reviewCount={reviewCount}
|
||||
reviewPanel={reviewPanel}
|
||||
activeDiff={activeReviewFile()}
|
||||
focusReviewDiff={focusReviewDiff}
|
||||
reviewSnap={ui.reviewSnap}
|
||||
size={size}
|
||||
/>
|
||||
</Suspense>
|
||||
<Show
|
||||
when={desktopBrowserBinding()}
|
||||
keyed
|
||||
fallback={
|
||||
<Suspense>
|
||||
<SessionSidePanel
|
||||
canReview={canReview}
|
||||
diffs={reviewDiffs}
|
||||
diffsReady={reviewReady}
|
||||
empty={reviewEmptyText}
|
||||
hasReview={hasReview}
|
||||
reviewHasFocusableContent={hasReview}
|
||||
reviewCount={reviewCount}
|
||||
reviewPanel={reviewPanel}
|
||||
activeDiff={activeReviewFile()}
|
||||
focusReviewDiff={focusReviewDiff}
|
||||
reviewSnap={ui.reviewSnap}
|
||||
size={size}
|
||||
/>
|
||||
</Suspense>
|
||||
}
|
||||
>
|
||||
{(registration) => <SessionBrowserPane registration={registration} />}
|
||||
</Show>
|
||||
</Show>
|
||||
<Show when={newSessionDesign()}>
|
||||
<Show when={isDesktop() ? desktopV2PanelLayout().visible : terminalOpen()}>
|
||||
<div class="min-w-0 h-full flex flex-1 flex-col">
|
||||
<Show when={isDesktop() && (desktopV2ReviewOpen() || desktopFileTreeOpen())}>
|
||||
<Show when={desktopBrowserBinding()} keyed>
|
||||
{(registration) => (
|
||||
<div class="min-h-0 flex-1">
|
||||
<SessionBrowserPane registration={registration} />
|
||||
</div>
|
||||
)}
|
||||
</Show>
|
||||
<Show when={isDesktop() && !desktopBrowserOpen() && (desktopV2ReviewOpen() || desktopFileTreeOpen())}>
|
||||
<div class="min-h-0 flex-1">
|
||||
<Suspense>
|
||||
<SessionSidePanel
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
import { useDialog } from "@opencode-ai/ui/context/dialog"
|
||||
import { createEffect, onCleanup, onMount } from "solid-js"
|
||||
import { createStore } from "solid-js/store"
|
||||
import { usePlatform, type BrowserPaneRegistration } from "@/context/platform"
|
||||
|
||||
export function SessionBrowserPane(props: { registration: BrowserPaneRegistration }) {
|
||||
const platform = usePlatform()
|
||||
const dialog = useDialog()
|
||||
const [store, setStore] = createStore({
|
||||
visible: typeof document === "undefined" || document.visibilityState === "visible",
|
||||
})
|
||||
let surface: HTMLDivElement | undefined
|
||||
let frame: number | undefined
|
||||
let until = 0
|
||||
|
||||
const measure = () => {
|
||||
frame = undefined
|
||||
if (!surface) return
|
||||
const rect = surface.getBoundingClientRect()
|
||||
const zoom = platform.webviewZoom?.() ?? 1
|
||||
const left = Math.round(rect.left * zoom)
|
||||
const top = Math.round(rect.top * zoom)
|
||||
const right = Math.round(rect.right * zoom)
|
||||
const bottom = Math.round(rect.bottom * zoom)
|
||||
props.registration.setLayout({
|
||||
visible: store.visible && !dialog.active,
|
||||
bounds: { x: left, y: top, width: Math.max(0, right - left), height: Math.max(0, bottom - top) },
|
||||
})
|
||||
if (performance.now() < until) frame = requestAnimationFrame(measure)
|
||||
}
|
||||
|
||||
const schedule = (duration = 0) => {
|
||||
until = Math.max(until, performance.now() + duration)
|
||||
if (frame !== undefined) return
|
||||
frame = requestAnimationFrame(measure)
|
||||
}
|
||||
|
||||
createEffect(() => {
|
||||
props.registration
|
||||
platform.webviewZoom?.()
|
||||
dialog.active
|
||||
store.visible
|
||||
schedule(300)
|
||||
})
|
||||
|
||||
onMount(() => {
|
||||
const resize = new ResizeObserver(() => schedule())
|
||||
if (surface) resize.observe(surface)
|
||||
const onResize = () => schedule(300)
|
||||
const onVisibility = () => setStore("visible", document.visibilityState === "visible")
|
||||
window.addEventListener("resize", onResize)
|
||||
document.addEventListener("visibilitychange", onVisibility)
|
||||
schedule(300)
|
||||
onCleanup(() => {
|
||||
resize.disconnect()
|
||||
window.removeEventListener("resize", onResize)
|
||||
document.removeEventListener("visibilitychange", onVisibility)
|
||||
if (frame !== undefined) cancelAnimationFrame(frame)
|
||||
props.registration.setLayout()
|
||||
})
|
||||
})
|
||||
|
||||
return <div ref={surface} class="size-full bg-background-base" />
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
import { describe, expect, test } from "bun:test"
|
||||
import { canRememberPermission } from "./session-permission-dock"
|
||||
|
||||
describe("canRememberPermission", () => {
|
||||
test("hides Always when the request has no persistent resources", () => {
|
||||
expect(canRememberPermission({ always: [] })).toBe(false)
|
||||
expect(canRememberPermission({ always: ["https://example.com/*"] })).toBe(true)
|
||||
})
|
||||
})
|
||||
@@ -5,6 +5,10 @@ import { DockPrompt } from "@opencode-ai/session-ui/dock-prompt"
|
||||
import { Icon } from "@opencode-ai/ui/icon"
|
||||
import { useLanguage } from "@/context/language"
|
||||
|
||||
export function canRememberPermission(request: Pick<PermissionRequest, "always">) {
|
||||
return request.always.length > 0
|
||||
}
|
||||
|
||||
export function SessionPermissionDock(props: {
|
||||
request: PermissionRequest
|
||||
responding: boolean
|
||||
@@ -37,14 +41,16 @@ export function SessionPermissionDock(props: {
|
||||
<Button variant="ghost" size="normal" onClick={() => props.onDecide("reject")} disabled={props.responding}>
|
||||
{language.t("ui.permission.deny")}
|
||||
</Button>
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="normal"
|
||||
onClick={() => props.onDecide("always")}
|
||||
disabled={props.responding}
|
||||
>
|
||||
{language.t("ui.permission.allowAlways")}
|
||||
</Button>
|
||||
<Show when={canRememberPermission(props.request)}>
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="normal"
|
||||
onClick={() => props.onDecide("always")}
|
||||
disabled={props.responding}
|
||||
>
|
||||
{language.t("ui.permission.allowAlways")}
|
||||
</Button>
|
||||
</Show>
|
||||
<Button variant="primary" size="normal" onClick={() => props.onDecide("once")} disabled={props.responding}>
|
||||
{language.t("ui.permission.allowOnce")}
|
||||
</Button>
|
||||
|
||||
@@ -3,15 +3,19 @@ import { sessionPanelLayout } from "./session-panel-layout"
|
||||
|
||||
describe("sessionPanelLayout", () => {
|
||||
test("keeps one V2 owner while changing panel geometry", () => {
|
||||
expect(sessionPanelLayout({ review: false, terminal: false, files: false })).toEqual({
|
||||
expect(sessionPanelLayout({ review: false, browser: false, terminal: false, files: false })).toEqual({
|
||||
visible: false,
|
||||
stacked: false,
|
||||
})
|
||||
expect(sessionPanelLayout({ review: false, terminal: true, files: false })).toEqual({
|
||||
expect(sessionPanelLayout({ review: false, browser: false, terminal: true, files: false })).toEqual({
|
||||
visible: true,
|
||||
stacked: false,
|
||||
})
|
||||
expect(sessionPanelLayout({ review: true, terminal: true, files: false })).toEqual({
|
||||
expect(sessionPanelLayout({ review: true, browser: false, terminal: true, files: false })).toEqual({
|
||||
visible: true,
|
||||
stacked: true,
|
||||
})
|
||||
expect(sessionPanelLayout({ review: false, browser: true, terminal: true, files: false })).toEqual({
|
||||
visible: true,
|
||||
stacked: true,
|
||||
})
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
export function sessionPanelLayout(input: { review: boolean; terminal: boolean; files: boolean }) {
|
||||
export function sessionPanelLayout(input: { review: boolean; browser: boolean; terminal: boolean; files: boolean }) {
|
||||
return {
|
||||
visible: input.review || input.terminal || input.files,
|
||||
stacked: input.review && input.terminal,
|
||||
visible: input.review || input.browser || input.terminal || input.files,
|
||||
stacked: (input.review || input.browser) && input.terminal,
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user