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: fix: sanitize absolute paths --- packages/desktop/src/components/file-tree.tsx | 3 +- packages/desktop/src/components/prompt-input.tsx | 2 +- packages/desktop/src/context/sync.tsx | 32 +- packages/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 - packages/enterprise/src/routes/share/[shareID].tsx | 403 +++++++++++---------- packages/ui/src/components/message-part.css | 11 + packages/ui/src/components/message-part.tsx | 73 ++-- packages/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, 309 insertions(+), 355 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 turns = () => ( +
+ {title()} +
+ + {(message) => ( + + )} + +
+
+ +
-
- ) + ) - const wide = createMemo(() => diffs().length === 0) + const wide = createMemo(() => diffs().length === 0) - return ( -
-
-
- - - -
-
- - -
-
-
-