Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
61 changes: 44 additions & 17 deletions packages/opencode/src/cli/cmd/tui/routes/session/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ import { Filesystem } from "@/util/filesystem"
import { PermissionPrompt } from "./permission"
import { DialogExportOptions } from "../../ui/dialog-export-options"
import { formatTranscript } from "../../util/transcript"
import { FilePathLink } from "../../ui/link"

addDefaultParsers(parsers.parsers)

Expand Down Expand Up @@ -1483,7 +1484,12 @@ function InlineTool(props: {
)
}

function BlockTool(props: { title: string; children: JSX.Element; onClick?: () => void; part?: ToolPart }) {
function BlockTool(props: {
title: string | JSX.Element
children: JSX.Element
onClick?: () => void
part?: ToolPart
}) {
const { theme } = useTheme()
const renderer = useRenderer()
const [hover, setHover] = createSignal(false)
Expand Down Expand Up @@ -1554,7 +1560,14 @@ function Write(props: ToolProps<typeof WriteTool>) {
return (
<Switch>
<Match when={props.metadata.diagnostics !== undefined}>
<BlockTool title={"# Wrote " + normalizePath(props.input.filePath!)} part={props.part}>
<BlockTool
title={
<>
# Wrote <FilePathLink path={props.input.filePath!}>{normalizePath(props.input.filePath!)}</FilePathLink>
</>
}
part={props.part}
>
<line_number fg={theme.textMuted} minWidth={3} paddingRight={1}>
<code
conceal={false}
Expand All @@ -1577,7 +1590,7 @@ function Write(props: ToolProps<typeof WriteTool>) {
</Match>
<Match when={true}>
<InlineTool icon="←" pending="Preparing write..." complete={props.input.filePath} part={props.part}>
Write {normalizePath(props.input.filePath!)}
Write <FilePathLink path={props.input.filePath!}>{normalizePath(props.input.filePath!)}</FilePathLink>
</InlineTool>
</Match>
</Switch>
Expand All @@ -1587,7 +1600,10 @@ function Write(props: ToolProps<typeof WriteTool>) {
function Glob(props: ToolProps<typeof GlobTool>) {
return (
<InlineTool icon="✱" pending="Finding files..." complete={props.input.pattern} part={props.part}>
Glob "{props.input.pattern}" <Show when={props.input.path}>in {normalizePath(props.input.path)} </Show>
Glob "{props.input.pattern}"{" "}
<Show when={props.input.path}>
in <FilePathLink path={props.input.path!}>{normalizePath(props.input.path)}</FilePathLink>{" "}
</Show>
<Show when={props.metadata.count}>({props.metadata.count} matches)</Show>
</InlineTool>
)
Expand All @@ -1596,38 +1612,40 @@ function Glob(props: ToolProps<typeof GlobTool>) {
function Read(props: ToolProps<typeof ReadTool>) {
return (
<InlineTool icon="→" pending="Reading file..." complete={props.input.filePath} part={props.part}>
Read {normalizePath(props.input.filePath!)} {input(props.input, ["filePath"])}
Read <FilePathLink path={props.input.filePath!}>{normalizePath(props.input.filePath!)}</FilePathLink>{" "}
{input(props.input, ["filePath"])}
</InlineTool>
)
}

function Grep(props: ToolProps<typeof GrepTool>) {
return (
<InlineTool icon="✱" pending="Searching content..." complete={props.input.pattern} part={props.part}>
Grep "{props.input.pattern}" <Show when={props.input.path}>in {normalizePath(props.input.path)} </Show>
Grep "{props.input.pattern}"{" "}
<Show when={props.input.path}>
in <FilePathLink path={props.input.path!}>{normalizePath(props.input.path)}</FilePathLink>{" "}
</Show>
<Show when={props.metadata.matches}>({props.metadata.matches} matches)</Show>
</InlineTool>
)
}

function List(props: ToolProps<typeof ListTool>) {
const dir = createMemo(() => {
if (props.input.path) {
return normalizePath(props.input.path)
}
return ""
})
return (
<InlineTool icon="→" pending="Listing directory..." complete={props.input.path !== undefined} part={props.part}>
List {dir()}
List{" "}
<Show when={props.input.path}>
<FilePathLink path={props.input.path!}>{normalizePath(props.input.path)}</FilePathLink>
</Show>
</InlineTool>
)
}

function WebFetch(props: ToolProps<typeof WebFetchTool>) {
const url = () => (props.input as any).url
return (
<InlineTool icon="%" pending="Fetching from the web..." complete={(props.input as any).url} part={props.part}>
WebFetch {(props.input as any).url}
<InlineTool icon="%" pending="Fetching from the web..." complete={url()} part={props.part}>
WebFetch <a href={url()}>{url()}</a>
</InlineTool>
)
}
Expand Down Expand Up @@ -1730,7 +1748,15 @@ function Edit(props: ToolProps<typeof EditTool>) {
return (
<Switch>
<Match when={props.metadata.diff !== undefined}>
<BlockTool title={"← Edit " + normalizePath(props.input.filePath!)} part={props.part}>
<BlockTool
title={
<>
{"← Edit "}
<FilePathLink path={props.input.filePath!}>{normalizePath(props.input.filePath!)}</FilePathLink>
</>
}
part={props.part}
>
<box paddingLeft={1}>
<diff
diff={diffContent()}
Expand Down Expand Up @@ -1768,7 +1794,8 @@ function Edit(props: ToolProps<typeof EditTool>) {
</Match>
<Match when={true}>
<InlineTool icon="←" pending="Preparing edit..." complete={props.input.filePath} part={props.part}>
Edit {normalizePath(props.input.filePath!)} {input({ replaceAll: props.input.replaceAll })}
Edit <FilePathLink path={props.input.filePath!}>{normalizePath(props.input.filePath!)}</FilePathLink>{" "}
{input({ replaceAll: props.input.replaceAll })}
</InlineTool>
</Match>
</Switch>
Expand Down
33 changes: 21 additions & 12 deletions packages/opencode/src/cli/cmd/tui/ui/link.tsx
Original file line number Diff line number Diff line change
@@ -1,28 +1,37 @@
import type { JSX } from "solid-js"
import type { RGBA } from "@opentui/core"
import open from "open"
import path from "path"

export interface LinkProps {
href: string
children?: JSX.Element | string
fg?: RGBA
}

/**
* Link component that renders clickable hyperlinks.
* Clicking anywhere on the link text opens the URL in the default browser.
*/
export function Link(props: LinkProps) {
const displayText = props.children ?? props.href

return (
<text
fg={props.fg}
onMouseUp={() => {
open(props.href).catch(() => {})
}}
>
<a href={props.href} style={{ fg: props.fg }}>
{displayText}
</text>
</a>
)
}

export interface FilePathLinkProps {
path: string
children?: JSX.Element | string
fg?: RGBA
}

export function FilePathLink(props: FilePathLinkProps) {
const displayText = props.children ?? props.path
const absolutePath = path.isAbsolute(props.path) ? props.path : path.resolve(process.cwd(), props.path)
const fileUrl = `file://${absolutePath}`

return (
<a href={fileUrl} style={{ fg: props.fg }}>
{displayText}
</a>
)
}