Compare commits

...
5 changed files with 79 additions and 11 deletions
@@ -55,7 +55,10 @@ test("opens and searches project files inline", async ({ page }) => {
{ name: "src", path: "src", absolute: `${directory}/src`, type: "directory", ignored: false },
]
},
fileContent: (path) => ({ type: "text", content: `contents:${path}` }),
fileContent: (path) => ({
type: "text",
content: path === "README.md" ? "# Rendered README\n\ncontents:README.md" : `contents:${path}`,
}),
findFiles: (input) => {
searches.push(input)
return input.query === "nested" ? ["src/nested.ts"] : []
@@ -126,7 +129,19 @@ test("opens and searches project files inline", async ({ page }) => {
await expect(panel.getByRole("tab", { name: "README.md" }).locator("..")).toHaveCSS("padding-inline-end", "4px")
await expect(panel.getByRole("tab", { name: "README.md" }).locator("..")).toHaveCSS("gap", "8px")
await expect(sidebarToggle).toBeEnabled()
await expect(panel.getByRole("heading", { name: "Rendered README" })).toBeVisible()
await expect(panel.getByText("contents:README.md", { exact: true })).toBeVisible()
await expect(panel.getByTitle("README.md")).toBeVisible()
const viewSource = panel.getByRole("button", { name: "View Source" })
await expect(viewSource).toBeVisible()
await expect.poll(() => viewSource.evaluate((button) => button.scrollWidth <= button.clientWidth)).toBe(true)
await viewSource.click()
await expect(panel.getByRole("heading", { name: "Rendered README" })).toHaveCount(0)
await expect(panel.getByText("# Rendered README", { exact: true })).toBeVisible()
const viewRendered = panel.getByRole("button", { name: "View Rendered" })
await expect.poll(() => viewRendered.evaluate((button) => button.scrollWidth <= button.clientWidth)).toBe(true)
await viewRendered.click()
await expect(panel.getByRole("heading", { name: "Rendered README" })).toBeVisible()
await expect(sidebar).toHaveCount(0)
const missingReadPattern = "**/api/fs/read/README.md*"
@@ -173,6 +188,8 @@ test("opens and searches project files inline", async ({ page }) => {
await expect(panel.getByRole("tab", { name: "nested.ts" }).locator("..")).toHaveCSS("gap", "8px")
await expect(sidebarToggle).toBeEnabled()
await expect(panel.getByText("contents:src/nested.ts", { exact: true })).toBeVisible()
await expect(panel.getByTitle("src/nested.ts")).toBeVisible()
await expect(panel.getByRole("button", { name: /View (Source|Rendered)/ })).toHaveCount(0)
expect(searches).toContainEqual({ query: "nested", dirs: "file", limit: 200 })
await panel.getByRole("button", { name: "Open file" }).click()
+2
View File
@@ -796,6 +796,8 @@ export const dict = {
"session.review.noChanges": "No changes",
"session.review.noUncommittedChanges": "No uncommitted changes yet",
"session.review.noBranchChanges": "No branch changes yet",
"session.files.markdown.viewSource": "View Source",
"session.files.markdown.viewRendered": "View Rendered",
"session.files.selectToOpen": "Select a file to open",
"session.files.all": "All files",
+51 -4
View File
@@ -1,9 +1,12 @@
import { createEffect, createMemo, createSignal, Match, on, onCleanup, Switch } from "solid-js"
import { createEffect, createMemo, createSignal, Match, on, onCleanup, Show, Switch } from "solid-js"
import { createStore } from "solid-js/store"
import { Dynamic } from "solid-js/web"
import { makeEventListener } from "@solid-primitives/event-listener"
import type { FileSearchHandle } from "@opencode/session-ui/file"
import { Markdown } from "@opencode/session-ui/markdown"
import { Button } from "@opencode/ui/button"
import { useFileComponent } from "@opencode/ui/context/file"
import { FileIcon } from "@opencode/ui/file-icon"
import { cloneSelectedLineRange, previewSelectedLines } from "@opencode/session-ui/pierre/selection-bridge"
import { createLineCommentControllerV2 } from "@opencode/session-ui/v2/line-comment-annotations-v2"
import { sampledChecksum } from "@opencode/util/encode"
@@ -19,6 +22,9 @@ import { useComposerState } from "@/composer/persistence"
import { getSessionHandoff } from "@/session/handoff"
import { useSessionLayout } from "@/session/session-layout"
import { createSessionTabs } from "@/session/helpers"
import { OpenInAppButton } from "@/session/files/open-in-app-button"
import { resolveOpenInAppPath } from "@/session/files/open-in-app-path"
import { useWorkspaceLocation } from "@/workspaces/location"
type SessionFileViewProps = {
tab: string
@@ -182,6 +188,7 @@ export function SessionFileView(props: SessionFileViewProps) {
const language = useLanguage()
const prompt = useComposerState()
const fileComponent = useFileComponent()
const location = useWorkspaceLocation()
const { sessionKey, tabs, view } = useSessionLayout()
const activeFileTab = createSessionTabs({
tabs,
@@ -198,6 +205,9 @@ export function SessionFileView(props: SessionFileViewProps) {
}
const path = createMemo(() => file.pathFromTab(props.tab))
const markdown = createMemo(() => path()?.toLowerCase().endsWith(".md") ?? false)
const absolutePath = createMemo(() => resolveOpenInAppPath(location().directory, path() ?? ""))
const [display, setDisplay] = createStore({ markdown: "rendered" as "rendered" | "source" })
const state = createMemo(() => {
const p = path()
if (!p) return
@@ -451,10 +461,47 @@ export function SessionFileView(props: SessionFileViewProps) {
)
const content = () => (
<div class="mt-3 relative h-full min-h-0">
<ScrollView class="h-full" viewportRef={scrollSync.setViewport} onScroll={scrollSync.handleScroll}>
<div class="relative h-full min-h-0 flex flex-col">
<Show when={path()}>
{(value) => (
<div data-slot="session-review-v2-file-header">
<div data-slot="session-review-v2-file-title">
<FileIcon node={{ path: value(), type: "file" }} class="size-4 shrink-0" />
<span class="min-w-0 flex-1 truncate text-13-regular text-text-muted" title={value()}>
{value()}
</span>
</div>
<div class="ms-auto shrink-0 flex items-center gap-3">
<Show when={markdown()}>
<Button
size="small"
variant="neutral"
class="min-w-[112px]"
onClick={() => setDisplay("markdown", display.markdown === "rendered" ? "source" : "rendered")}
>
{language.t(
display.markdown === "rendered"
? "session.files.markdown.viewSource"
: "session.files.markdown.viewRendered",
)}
</Button>
</Show>
<OpenInAppButton path={absolutePath} reveal />
</div>
</div>
)}
</Show>
<ScrollView class="flex-1 min-h-0" viewportRef={scrollSync.setViewport} onScroll={scrollSync.handleScroll}>
<Switch>
<Match when={state()?.loaded}>{renderFile(contents())}</Match>
<Match when={state()?.loaded}>
{markdown() && display.markdown === "rendered" ? (
<div class="px-6 py-4 pb-40">
<Markdown text={contents()} cacheKey={cacheKey()} />
</div>
) : (
renderFile(contents())
)}
</Match>
<Match when={state()?.loading}>
<div class="px-6 py-4 text-text-weak">{language.t("common.loading")}</div>
</Match>
@@ -8,12 +8,12 @@ import { Tooltip } from "@opencode/ui/tooltip"
import { useLanguage } from "@/runtime/i18n/language"
import { type OpenApp, useOpenInApp } from "@/session/files/open-in-app"
export function OpenInAppButton(props: { directory: () => string }) {
export function OpenInAppButton(props: { path: () => string; reveal?: boolean }) {
const language = useLanguage()
const state = useOpenInApp({ path: props.directory })
const state = useOpenInApp({ path: props.path })
return (
<Show when={props.directory() && state.canOpen()}>
<Show when={props.path() && state.canOpen()}>
<SplitButton class="session-review-v2-open-in-app" onPointerDown={(event) => event.stopPropagation()}>
<Tooltip
placement="bottom"
@@ -25,7 +25,7 @@ export function OpenInAppButton(props: { directory: () => string }) {
onClick={(event) => {
event.stopPropagation()
if (state.opening()) return
state.openPath(state.current().id)
state.openPath(state.current().id, undefined, props.reveal)
}}
disabled={state.opening()}
aria-label={language.t("session.header.open.ariaLabel", { app: state.current().label })}
@@ -52,7 +52,7 @@ export function OpenInAppButton(props: { directory: () => string }) {
</Menu.Trigger>
<Menu.Portal>
<Menu.Content class="open-in-app-v2-menu">
<OpenInAppMenuItemsV2 state={state} close={() => state.setMenu("open", false)} />
<OpenInAppMenuItemsV2 state={state} reveal={props.reveal} close={() => state.setMenu("open", false)} />
</Menu.Content>
</Menu.Portal>
</Menu>
@@ -542,7 +542,9 @@ export function SessionSidePanel(props: {
onPointerDown={(event) => event.stopPropagation()}
onClick={(event) => event.stopPropagation()}
>
<OpenInAppButton directory={projectDirectory} />
<Show when={!file.pathFromTab(activeTab())}>
<OpenInAppButton path={projectDirectory} />
</Show>
<Show when={reviewVisible()}>
<div class="size-7 shrink-0" aria-hidden />
</Show>