diff options
| author | Adam <[email protected]> | 2025-11-25 16:02:27 -0600 |
|---|---|---|
| committer | Adam <[email protected]> | 2025-11-25 16:02:31 -0600 |
| commit | 447713244820e875e192a5815c4d8bc76c03b40f (patch) | |
| tree | a3df06b1df0585a8387c37826ea516ce08169289 /packages/ui | |
| parent | eaeea45ace01e20405d49d1a659030a4d131dc83 (diff) | |
| download | opencode-447713244820e875e192a5815c4d8bc76c03b40f.tar.gz opencode-447713244820e875e192a5815c4d8bc76c03b40f.zip | |
fix: sanitize absolute paths
Diffstat (limited to 'packages/ui')
| -rw-r--r-- | packages/ui/src/components/message-part.css | 11 | ||||
| -rw-r--r-- | packages/ui/src/components/message-part.tsx | 73 | ||||
| -rw-r--r-- | packages/ui/src/components/message-progress.tsx | 3 | ||||
| -rw-r--r-- | packages/ui/src/components/session-turn.tsx | 6 | ||||
| -rw-r--r-- | packages/ui/src/context/data.tsx | 4 |
5 files changed, 63 insertions, 34 deletions
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<MessagePartProps> export const PART_MAPPING: Record<string, PartComponent | undefined> = {} -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) { </Match> <Match when={props.message.role === "assistant" && props.message}> {(assistantMessage) => ( - <AssistantMessageDisplay message={assistantMessage() as AssistantMessage} parts={props.parts} /> + <AssistantMessageDisplay + message={assistantMessage() as AssistantMessage} + parts={props.parts} + sanitize={props.sanitize} + /> )} </Match> </Switch> ) } -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 <For each={filteredParts()}>{(part) => <Part part={part} message={props.message} />}</For> + return ( + <For each={filteredParts()}>{(part) => <Part part={part} message={props.message} sanitize={props.sanitize} />}</For> + ) } 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 ( <Show when={component()}> - <Dynamic component={component()} part={props.part} message={props.message} hideDetails={props.hideDetails} /> + <Dynamic + component={component()} + part={props.part} + message={props.message} + hideDetails={props.hideDetails} + sanitize={props.sanitize} + /> </Show> ) } @@ -99,6 +102,7 @@ export interface ToolProps { tool: string output?: string hideDetails?: boolean + sanitize?: RegExp } export type ToolComponent = Component<ToolProps> @@ -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} /> </Match> </Switch> @@ -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 ( <Show when={part.text.trim()}> <div data-component="text-part"> - <Markdown text={part.text.trim()} /> + <Markdown text={sanitized().text.trim()} /> </div> </Show> ) @@ -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 ( - <BasicTool icon="bullet-list" trigger={{ title: "List", subtitle: getDirectory(props.input.path || "/") }}> + <BasicTool + icon="bullet-list" + trigger={{ title: "List", subtitle: getDirectory(sanitize(props.input.path, props.sanitize) || "/") }} + > <Show when={false && props.output}> - <div data-component="tool-output">{props.output}</div> + <div data-component="tool-output">{sanitize(props.output, props.sanitize)}</div> </Show> </BasicTool> ) @@ -321,12 +330,14 @@ ToolRegistry.register({ icon="console" trigger={{ title: "Shell", - subtitle: "Ran " + props.input.command, + subtitle: props.input.description, }} > - <Show when={false && props.output}> - <div data-component="tool-output">{props.output}</div> - </Show> + <div data-component="tool-output"> + <Markdown + text={`\`\`\`command\n$ ${sanitize(props.input.command, props.sanitize)}${props.output ? "\n\n" + props.output : ""}\n\`\`\``} + /> + </div> </BasicTool> ) }, @@ -344,9 +355,13 @@ ToolRegistry.register({ <div data-slot="message-part-title">Edit</div> <div data-slot="message-part-path"> <Show when={props.input.filePath?.includes("/")}> - <span data-slot="message-part-directory">{getDirectory(props.input.filePath!)}</span> + <span data-slot="message-part-directory"> + {getDirectory(sanitize(props.input.filePath!, props.sanitize))} + </span> </Show> - <span data-slot="message-part-filename">{getFilename(props.input.filePath ?? "")}</span> + <span data-slot="message-part-filename"> + {getFilename(sanitize(props.input.filePath ?? "", props.sanitize))} + </span> </div> </div> <div data-slot="message-part-actions"> @@ -361,11 +376,11 @@ ToolRegistry.register({ <div data-component="edit-content"> <Diff before={{ - name: getFilename(props.metadata.filediff.path), + name: getFilename(sanitize(props.metadata.filediff.path, props.sanitize)), contents: props.metadata.filediff.before, }} after={{ - name: getFilename(props.metadata.filediff.path), + name: getFilename(sanitize(props.metadata.filediff.path, props.sanitize)), contents: props.metadata.filediff.after, }} /> 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 ( <div data-slot="message-progress-item"> - <Part message={message()!} part={part} /> + <Part message={message()!} part={part} sanitize={sanitizer()} /> </div> ) }} 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( </div> </div> <div data-slot="session-turn-message-content"> - <Message message={msg()} parts={parts()} /> + <Message message={msg()} parts={parts()} sanitize={sanitizer()} /> </div> {/* Summary */} <Show when={completed()}> @@ -222,10 +223,11 @@ export function SessionTurn( <Message message={assistantMessage} parts={parts().filter((p) => p?.id !== last()?.id)} + sanitize={sanitizer()} /> ) } - return <Message message={assistantMessage} parts={parts()} /> + return <Message message={assistantMessage} parts={parts()} sanitize={sanitizer()} /> }} </For> <Show when={error()}> 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 } }, }) |
