From 447713244820e875e192a5815c4d8bc76c03b40f Mon Sep 17 00:00:00 2001 From: Adam <2363879+adamdotdevin@users.noreply.github.com> Date: Tue, 25 Nov 2025 16:02:27 -0600 Subject: [PATCH] fix: sanitize absolute paths --- packages/desktop/src/components/file-tree.tsx | 3 +- .../desktop/src/components/prompt-input.tsx | 2 +- packages/desktop/src/context/sync.tsx | 32 +- .../desktop/src/pages/directory-layout.tsx | 2 +- packages/desktop/src/pages/home.tsx | 3 +- packages/desktop/src/pages/layout.tsx | 3 +- packages/desktop/src/pages/session.tsx | 2 +- packages/desktop/src/ui/collapsible.tsx | 62 --- packages/desktop/src/ui/index.ts | 6 - packages/desktop/src/utils/index.ts | 1 - packages/desktop/src/utils/path.ts | 20 - .../enterprise/src/routes/share/[shareID].tsx | 409 +++++++++--------- packages/ui/src/components/message-part.css | 11 + packages/ui/src/components/message-part.tsx | 73 ++-- .../ui/src/components/message-progress.tsx | 3 +- packages/ui/src/components/session-turn.tsx | 6 +- packages/ui/src/context/data.tsx | 4 +- packages/util/src/sanitize.ts | 28 ++ 18 files changed, 312 insertions(+), 358 deletions(-) delete mode 100644 packages/desktop/src/ui/collapsible.tsx delete mode 100644 packages/desktop/src/ui/index.ts delete mode 100644 packages/desktop/src/utils/path.ts create mode 100644 packages/util/src/sanitize.ts diff --git a/packages/desktop/src/components/file-tree.tsx b/packages/desktop/src/components/file-tree.tsx index f3729a8d3..0841c71d1 100644 --- a/packages/desktop/src/components/file-tree.tsx +++ b/packages/desktop/src/components/file-tree.tsx @@ -1,5 +1,5 @@ import { useLocal, type LocalFile } from "@/context/local" -import { Collapsible } from "@/ui" +import { Collapsible } from "@opencode-ai/ui/collapsible" import { FileIcon } from "@opencode-ai/ui/file-icon" import { Tooltip } from "@opencode-ai/ui/tooltip" import { For, Match, Switch, Show, type ComponentProps, type ParentProps } from "solid-js" @@ -76,6 +76,7 @@ export default function FileTree(props: { p())).then(() => setStore("ready", true)) - const sanitizer = createMemo(() => new RegExp(`${store.path.directory}/`, "g")) - const sanitize = (text: string) => text.replace(sanitizer(), "") const absolute = (path: string) => (store.path.directory + "/" + path).replace("//", "/") - const sanitizePart = (part: Part) => { - if (part.type === "tool") { - if (part.state.status === "completed" || part.state.status === "error") { - for (const key in part.state.metadata) { - if (typeof part.state.metadata[key] === "string") { - part.state.metadata[key] = sanitize(part.state.metadata[key] as string) - } - } - for (const key in part.state.input) { - if (typeof part.state.input[key] === "string") { - part.state.input[key] = sanitize(part.state.input[key] as string) - } - } - if ("error" in part.state) { - part.state.error = sanitize(part.state.error as string) - } - } - } - return part - } return { data: store, @@ -88,10 +65,7 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({ .slice() .sort((a, b) => a.id.localeCompare(b.id)) for (const message of messages.data!) { - draft.part[message.info.id] = message.parts - .slice() - .map(sanitizePart) - .sort((a, b) => a.id.localeCompare(b.id)) + draft.part[message.info.id] = message.parts.slice().sort((a, b) => a.id.localeCompare(b.id)) } draft.session_diff[sessionID] = diff.data ?? [] }), @@ -105,7 +79,9 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({ }, load, absolute, - sanitize, + get directory() { + return store.path.directory + }, } }, }) diff --git a/packages/desktop/src/pages/directory-layout.tsx b/packages/desktop/src/pages/directory-layout.tsx index 2fe750fda..de16eff30 100644 --- a/packages/desktop/src/pages/directory-layout.tsx +++ b/packages/desktop/src/pages/directory-layout.tsx @@ -21,7 +21,7 @@ export default function Layout(props: ParentProps) { {iife(() => { const sync = useSync() return ( - + {props.children} ) diff --git a/packages/desktop/src/pages/home.tsx b/packages/desktop/src/pages/home.tsx index e773fff57..58fcb20ce 100644 --- a/packages/desktop/src/pages/home.tsx +++ b/packages/desktop/src/pages/home.tsx @@ -1,8 +1,9 @@ import { useGlobalSync } from "@/context/global-sync" -import { base64Encode, getFilename } from "@/utils" +import { base64Encode } from "@/utils" import { For } from "solid-js" import { A } from "@solidjs/router" import { Button } from "@opencode-ai/ui/button" +import { getFilename } from "@opencode-ai/util/path" export default function Home() { const sync = useGlobalSync() diff --git a/packages/desktop/src/pages/layout.tsx b/packages/desktop/src/pages/layout.tsx index c9bb559d8..15180c885 100644 --- a/packages/desktop/src/pages/layout.tsx +++ b/packages/desktop/src/pages/layout.tsx @@ -3,7 +3,7 @@ import { DateTime } from "luxon" import { A, useParams } from "@solidjs/router" import { useLayout } from "@/context/layout" import { useGlobalSync } from "@/context/global-sync" -import { base64Encode, getFilename } from "@/utils" +import { base64Encode } from "@/utils" import { Mark } from "@opencode-ai/ui/logo" import { Button } from "@opencode-ai/ui/button" import { Icon } from "@opencode-ai/ui/icon" @@ -11,6 +11,7 @@ import { IconButton } from "@opencode-ai/ui/icon-button" import { Tooltip } from "@opencode-ai/ui/tooltip" import { Collapsible } from "@opencode-ai/ui/collapsible" import { DiffChanges } from "@opencode-ai/ui/diff-changes" +import { getFilename } from "@opencode-ai/util/path" export default function Layout(props: ParentProps) { const params = useParams() diff --git a/packages/desktop/src/pages/session.tsx b/packages/desktop/src/pages/session.tsx index 0eb0f37ce..40acac663 100644 --- a/packages/desktop/src/pages/session.tsx +++ b/packages/desktop/src/pages/session.tsx @@ -1,7 +1,6 @@ import { For, onCleanup, onMount, Show, Match, Switch, createResource, createMemo } from "solid-js" import { useLocal, type LocalFile } from "@/context/local" import { createStore } from "solid-js/store" -import { getDirectory, getFilename } from "@/utils" import { PromptInput } from "@/components/prompt-input" import { DateTime } from "luxon" import { FileIcon } from "@opencode-ai/ui/file-icon" @@ -30,6 +29,7 @@ import type { JSX } from "solid-js" import { useSync } from "@/context/sync" import { useSession } from "@/context/session" import { useLayout } from "@/context/layout" +import { getDirectory, getFilename } from "@opencode-ai/util/path" export default function Page() { const layout = useLayout() diff --git a/packages/desktop/src/ui/collapsible.tsx b/packages/desktop/src/ui/collapsible.tsx deleted file mode 100644 index 5fbb6c7a4..000000000 --- a/packages/desktop/src/ui/collapsible.tsx +++ /dev/null @@ -1,62 +0,0 @@ -import { Collapsible as KobalteCollapsible } from "@kobalte/core/collapsible" -import { Icon, IconProps } from "@opencode-ai/ui/icon" -import { splitProps } from "solid-js" -import type { ComponentProps, ParentProps } from "solid-js" - -export interface CollapsibleProps extends ComponentProps {} -export interface CollapsibleTriggerProps extends ComponentProps {} -export interface CollapsibleContentProps extends ComponentProps {} - -function CollapsibleRoot(props: CollapsibleProps) { - return -} - -function CollapsibleTrigger(props: CollapsibleTriggerProps) { - const [local, others] = splitProps(props, ["class"]) - return ( - - ) -} - -function CollapsibleContent(props: ParentProps) { - const [local, others] = splitProps(props, ["class", "children"]) - return ( - - {local.children} - - ) -} - -function CollapsibleArrow(props: Partial) { - const [local, others] = splitProps(props, ["class", "name"]) - return ( - - ) -} - -export const Collapsible = Object.assign(CollapsibleRoot, { - Trigger: CollapsibleTrigger, - Content: CollapsibleContent, - Arrow: CollapsibleArrow, -}) diff --git a/packages/desktop/src/ui/index.ts b/packages/desktop/src/ui/index.ts deleted file mode 100644 index 8cbf0834f..000000000 --- a/packages/desktop/src/ui/index.ts +++ /dev/null @@ -1,6 +0,0 @@ -export { - Collapsible, - type CollapsibleProps, - type CollapsibleTriggerProps, - type CollapsibleContentProps, -} from "./collapsible" diff --git a/packages/desktop/src/utils/index.ts b/packages/desktop/src/utils/index.ts index 63a656cc4..e50efe837 100644 --- a/packages/desktop/src/utils/index.ts +++ b/packages/desktop/src/utils/index.ts @@ -1,3 +1,2 @@ -export * from "./path" export * from "./dom" export * from "./encode" diff --git a/packages/desktop/src/utils/path.ts b/packages/desktop/src/utils/path.ts deleted file mode 100644 index d23568ae6..000000000 --- a/packages/desktop/src/utils/path.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { useSync } from "@/context/sync" - -export function getFilename(path: string) { - if (!path) return "" - const trimmed = path.replace(/[\/]+$/, "") - const parts = trimmed.split("/") - return parts[parts.length - 1] ?? "" -} - -export function getDirectory(path: string) { - const sync = useSync() - const parts = path.split("/") - const dir = parts.slice(0, parts.length - 1).join("/") - return dir ? sync.sanitize(dir + "/") : "" -} - -export function getFileExtension(path: string) { - const parts = path.split(".") - return parts[parts.length - 1] -} diff --git a/packages/enterprise/src/routes/share/[shareID].tsx b/packages/enterprise/src/routes/share/[shareID].tsx index 292038094..42dd7e957 100644 --- a/packages/enterprise/src/routes/share/[shareID].tsx +++ b/packages/enterprise/src/routes/share/[shareID].tsx @@ -141,219 +141,226 @@ export default function () { }} > - {(data) => ( - - {iife(() => { - const [store, setStore] = createStore({ - messageId: undefined as string | undefined, - }) - const match = createMemo(() => Binary.search(data().session, data().sessionID, (s) => s.id)) - if (!match().found) throw new Error(`Session ${data().sessionID} not found`) - const info = createMemo(() => data().session[match().index]) - const messages = createMemo(() => - data().sessionID - ? (data().message[data().sessionID]?.filter((m) => m.role === "user") ?? []).sort( - (a, b) => b.time.created - a.time.created, - ) - : [], - ) - const firstUserMessage = createMemo(() => messages().at(0)) - const activeMessage = createMemo( - () => messages().find((m) => m.id === store.messageId) ?? firstUserMessage(), - ) - function setActiveMessage(message: UserMessage | undefined) { - if (message) { - setStore("messageId", message.id) - } else { - setStore("messageId", undefined) + {(data) => { + const match = createMemo(() => Binary.search(data().session, data().sessionID, (s) => s.id)) + if (!match().found) throw new Error(`Session ${data().sessionID} not found`) + const info = createMemo(() => data().session[match().index]) + + return ( + + {iife(() => { + const [store, setStore] = createStore({ + messageId: undefined as string | undefined, + }) + const messages = createMemo(() => + data().sessionID + ? (data().message[data().sessionID]?.filter((m) => m.role === "user") ?? []).sort( + (a, b) => b.time.created - a.time.created, + ) + : [], + ) + const firstUserMessage = createMemo(() => messages().at(0)) + const activeMessage = createMemo( + () => messages().find((m) => m.id === store.messageId) ?? firstUserMessage(), + ) + function setActiveMessage(message: UserMessage | undefined) { + if (message) { + setStore("messageId", message.id) + } else { + setStore("messageId", undefined) + } } - } - const provider = createMemo(() => activeMessage()?.model?.providerID) - const modelID = createMemo(() => activeMessage()?.model?.modelID) - const model = createMemo(() => data().model[data().sessionID]?.find((m) => m.id === modelID())) - const diffs = createMemo(() => { - const diffs = data().session_diff[data().sessionID] ?? [] - const preloaded = data().session_diff_preload[data().sessionID] ?? [] - return diffs.map((diff) => ({ - ...diff, - preloaded: preloaded.find((d) => d.newFile.name === diff.file), - })) - }) + const provider = createMemo(() => activeMessage()?.model?.providerID) + const modelID = createMemo(() => activeMessage()?.model?.modelID) + const model = createMemo(() => data().model[data().sessionID]?.find((m) => m.id === modelID())) + const diffs = createMemo(() => { + const diffs = data().session_diff[data().sessionID] ?? [] + const preloaded = data().session_diff_preload[data().sessionID] ?? [] + return diffs.map((diff) => ({ + ...diff, + preloaded: preloaded.find((d) => d.newFile.name === diff.file), + })) + }) - const title = () => ( -
-
-
- -
v{info().version}
-
-
- -
{model()?.name ?? modelID()}
-
-
- {DateTime.fromMillis(info().time.created).toFormat("dd MMM yyyy, HH:mm")} + const title = () => ( +
+
+
+ +
v{info().version}
+
+
+ +
{model()?.name ?? modelID()}
+
+
+ {DateTime.fromMillis(info().time.created).toFormat("dd MMM yyyy, HH:mm")} +
+
{info().title}
-
{info().title}
-
- ) + ) - const turns = () => ( -
- {title()} -
- - {(message) => ( - - )} - -
-
- -
-
- ) - - const wide = createMemo(() => diffs().length === 0) - - return ( -
-
-
- - - -
-
- - -
-
-
- - ) - })} - - )} + ) + + const wide = createMemo(() => diffs().length === 0) + + return ( +
+
+
+ + + +
+
+ + +
+
+
+ + + 0}> + + + + Session + + + 5 Files Changed + + + + {turns()} + + + + + +
{turns()}
+
+
+
+
+ ) + })} + + ) + }} ) diff --git a/packages/ui/src/components/message-part.css b/packages/ui/src/components/message-part.css index dd6166112..0b1e8d490 100644 --- a/packages/ui/src/components/message-part.css +++ b/packages/ui/src/components/message-part.css @@ -63,6 +63,17 @@ [data-component="tool-output"] { white-space: pre; + padding: 8px 12px; + height: fit-content; + display: flex; + flex-direction: column; + align-items: flex-start; + justify-content: flex-start; + + pre { + margin: 0; + padding: 0; + } } [data-component="edit-trigger"], diff --git a/packages/ui/src/components/message-part.tsx b/packages/ui/src/components/message-part.tsx index dd920f101..40740fa1f 100644 --- a/packages/ui/src/components/message-part.tsx +++ b/packages/ui/src/components/message-part.tsx @@ -16,35 +16,26 @@ import { Checkbox } from "./checkbox" import { Diff } from "./diff" import { DiffChanges } from "./diff-changes" import { Markdown } from "./markdown" +import { getDirectory, getFilename } from "@opencode-ai/util/path" +import { sanitize, sanitizePart } from "@opencode-ai/util/sanitize" export interface MessageProps { message: MessageType parts: PartType[] + sanitize?: RegExp } export interface MessagePartProps { part: PartType message: MessageType hideDetails?: boolean + sanitize?: RegExp } export type PartComponent = Component export const PART_MAPPING: Record = {} -function getFilename(path: string) { - if (!path) return "" - const trimmed = path.replace(/[\/]+$/, "") - const parts = trimmed.split("/") - return parts[parts.length - 1] ?? "" -} - -function getDirectory(path: string) { - const parts = path.split("/") - const dir = parts.slice(0, parts.length - 1).join("/") - return dir ? dir + "/" : "" -} - export function registerPartComponent(type: string, component: PartComponent) { PART_MAPPING[type] = component } @@ -57,21 +48,27 @@ export function Message(props: MessageProps) { {(assistantMessage) => ( - + )} ) } -export function AssistantMessageDisplay(props: { message: AssistantMessage; parts: PartType[] }) { +export function AssistantMessageDisplay(props: { message: AssistantMessage; parts: PartType[]; sanitize?: RegExp }) { const filteredParts = createMemo(() => { return props.parts?.filter((x) => { if (x.type === "reasoning") return false return x.type !== "tool" || (x as ToolPart).tool !== "todoread" }) }) - return {(part) => } + return ( + {(part) => } + ) } export function UserMessageDisplay(props: { message: UserMessage; parts: PartType[] }) { @@ -88,7 +85,13 @@ export function Part(props: MessagePartProps) { const component = createMemo(() => PART_MAPPING[props.part.type]) return ( - + ) } @@ -99,6 +102,7 @@ export interface ToolProps { tool: string output?: string hideDetails?: boolean + sanitize?: RegExp } export type ToolComponent = Component @@ -166,6 +170,7 @@ PART_MAPPING["tool"] = function ToolPartDisplay(props) { metadata={metadata} output={part.state.status === "completed" ? part.state.output : undefined} hideDetails={props.hideDetails} + sanitize={props.sanitize} /> @@ -177,10 +182,11 @@ PART_MAPPING["tool"] = function ToolPartDisplay(props) { PART_MAPPING["text"] = function TextPartDisplay(props) { const part = props.part as TextPart + const sanitized = createMemo(() => (props.sanitize ? (sanitizePart(part, props.sanitize) as TextPart) : part)) return (
- +
) @@ -205,7 +211,7 @@ ToolRegistry.register({ icon="glasses" trigger={{ title: "Read", - subtitle: props.input.filePath ? getFilename(props.input.filePath) : "", + subtitle: props.input.filePath ? getFilename(sanitize(props.input.filePath, props.sanitize)) : "", }} /> ) @@ -216,9 +222,12 @@ ToolRegistry.register({ name: "list", render(props) { return ( - + -
{props.output}
+
{sanitize(props.output, props.sanitize)}
) @@ -321,12 +330,14 @@ ToolRegistry.register({ icon="console" trigger={{ title: "Shell", - subtitle: "Ran " + props.input.command, + subtitle: props.input.description, }} > - -
{props.output}
-
+
+ +
) }, @@ -344,9 +355,13 @@ ToolRegistry.register({
Edit
- {getDirectory(props.input.filePath!)} + + {getDirectory(sanitize(props.input.filePath!, props.sanitize))} + - {getFilename(props.input.filePath ?? "")} + + {getFilename(sanitize(props.input.filePath ?? "", props.sanitize))} +
@@ -361,11 +376,11 @@ ToolRegistry.register({
diff --git a/packages/ui/src/components/message-progress.tsx b/packages/ui/src/components/message-progress.tsx index ca42d26ec..adb245ab4 100644 --- a/packages/ui/src/components/message-progress.tsx +++ b/packages/ui/src/components/message-progress.tsx @@ -6,6 +6,7 @@ import type { AssistantMessage as AssistantMessageType, ToolPart } from "@openco export function MessageProgress(props: { assistantMessages: () => AssistantMessageType[]; done?: boolean }) { const data = useData() + const sanitizer = createMemo(() => (data.directory ? new RegExp(`${data.directory}/`, "g") : undefined)) const parts = createMemo(() => props.assistantMessages().flatMap((m) => data.part[m.id])) const done = createMemo(() => props.done ?? false) const currentTask = createMemo( @@ -152,7 +153,7 @@ export function MessageProgress(props: { assistantMessages: () => AssistantMessa ) return (
- +
) }} diff --git a/packages/ui/src/components/session-turn.tsx b/packages/ui/src/components/session-turn.tsx index a7bd456a4..d146bae95 100644 --- a/packages/ui/src/components/session-turn.tsx +++ b/packages/ui/src/components/session-turn.tsx @@ -31,6 +31,7 @@ export function SessionTurn( const match = Binary.search(data.session, props.sessionID, (s) => s.id) if (!match.found) throw new Error(`Session ${props.sessionID} not found`) + const sanitizer = createMemo(() => (data.directory ? new RegExp(`${data.directory}/`, "g") : undefined)) const messages = createMemo(() => (props.sessionID ? (data.message[props.sessionID] ?? []) : [])) const userMessages = createMemo(() => messages() @@ -116,7 +117,7 @@ export function SessionTurn(
- +
{/* Summary */} @@ -222,10 +223,11 @@ export function SessionTurn( p?.id !== last()?.id)} + sanitize={sanitizer()} /> ) } - return + return }} diff --git a/packages/ui/src/context/data.tsx b/packages/ui/src/context/data.tsx index c2766a5af..ad5a212ab 100644 --- a/packages/ui/src/context/data.tsx +++ b/packages/ui/src/context/data.tsx @@ -23,7 +23,7 @@ type Data = { export const { use: useData, provider: DataProvider } = createSimpleContext({ name: "Data", - init: (props: { data: Data }) => { - return props.data + init: (props: { data: Data; directory: string }) => { + return { ...props.data, directory: props.directory } }, }) diff --git a/packages/util/src/sanitize.ts b/packages/util/src/sanitize.ts new file mode 100644 index 000000000..892fe87cf --- /dev/null +++ b/packages/util/src/sanitize.ts @@ -0,0 +1,28 @@ +import { Part } from "@opencode-ai/sdk" + +export const sanitize = (text: string | undefined, remove?: RegExp) => (remove ? text?.replace(remove, "") : text) ?? "" + +export const sanitizePart = (part: Part, remove: RegExp) => { + if (part.type === "text") { + part.text = sanitize(part.text, remove) + } else if (part.type === "reasoning") { + part.text = sanitize(part.text, remove) + } else if (part.type === "tool") { + if (part.state.status === "completed" || part.state.status === "error") { + for (const key in part.state.metadata) { + if (typeof part.state.metadata[key] === "string") { + part.state.metadata[key] = sanitize(part.state.metadata[key] as string, remove) + } + } + for (const key in part.state.input) { + if (typeof part.state.input[key] === "string") { + part.state.input[key] = sanitize(part.state.input[key] as string, remove) + } + } + if ("error" in part.state) { + part.state.error = sanitize(part.state.error as string, remove) + } + } + } + return part +}