diff options
| author | Adam <[email protected]> | 2025-10-28 15:29:11 -0500 |
|---|---|---|
| committer | Adam <[email protected]> | 2025-10-28 15:29:16 -0500 |
| commit | 545f34584847e3216ecf95ab493dacac282ec465 (patch) | |
| tree | 9bf9c1e8cd2d49ee63a155e4b4131b4e1960fc0f /packages/desktop/src/components | |
| parent | 77ae0b527e4b1a8277b0391d0acbc7d82b08e4ea (diff) | |
| download | opencode-545f34584847e3216ecf95ab493dacac282ec465.tar.gz opencode-545f34584847e3216ecf95ab493dacac282ec465.zip | |
wip: desktop work
Diffstat (limited to 'packages/desktop/src/components')
| -rw-r--r-- | packages/desktop/src/components/assistant-message.tsx | 33 | ||||
| -rw-r--r-- | packages/desktop/src/components/prompt-input.tsx | 59 |
2 files changed, 62 insertions, 30 deletions
diff --git a/packages/desktop/src/components/assistant-message.tsx b/packages/desktop/src/components/assistant-message.tsx index 738293405..224e3d390 100644 --- a/packages/desktop/src/components/assistant-message.tsx +++ b/packages/desktop/src/components/assistant-message.tsx @@ -17,8 +17,15 @@ import type { WriteTool } from "opencode/tool/write" import type { TodoWriteTool } from "opencode/tool/todo" import { DiffChanges } from "./diff-changes" -export function AssistantMessage(props: { message: AssistantMessage; parts: Part[] }) { - const filteredParts = createMemo(() => props.parts?.filter((x) => x.type !== "tool" || x.tool !== "todoread")) +export function AssistantMessage(props: { message: AssistantMessage; parts: Part[]; lastToolOnly?: boolean }) { + const filteredParts = createMemo(() => { + let tool = false + return props.parts?.filter((x) => { + if (x.type === "tool" && props.lastToolOnly && tool) return false + if (x.type === "tool") tool = true + return x.type !== "tool" || x.tool !== "todoread" + }) + }) return ( <div class="w-full flex flex-col items-start gap-4"> <For each={filteredParts()}> @@ -71,17 +78,14 @@ function ToolPart(props: { part: ToolPart; message: AssistantMessage }) { // const permission = permissions[permissionIndex] return ( - <> - <Dynamic - component={render} - input={input} - tool={props.part.tool} - metadata={metadata} - // permission={permission?.metadata ?? {}} - output={props.part.state.status === "completed" ? props.part.state.output : undefined} - /> - {/* <Show when={props.part.state.status === "error"}>{props.part.state.error.replace("Error: ", "")}</Show> */} - </> + <Dynamic + component={render} + input={input} + tool={props.part.tool} + metadata={metadata} + // permission={permission?.metadata ?? {}} + output={props.part.state.status === "completed" ? props.part.state.output : undefined} + /> ) }) @@ -166,6 +170,9 @@ function BasicTool(props: { icon: IconProps["name"]; trigger: TriggerTitle | JSX <Collapsible.Content>{props.children}</Collapsible.Content> </Show> </Collapsible> + // <> + // <Show when={props.part.state.status === "error"}>{props.part.state.error.replace("Error: ", "")}</Show> + // </> ) } diff --git a/packages/desktop/src/components/prompt-input.tsx b/packages/desktop/src/components/prompt-input.tsx index dd549a045..e6701bddc 100644 --- a/packages/desktop/src/components/prompt-input.tsx +++ b/packages/desktop/src/components/prompt-input.tsx @@ -10,13 +10,15 @@ import { DateTime } from "luxon" interface PartBase { content: string + start: number + end: number } -interface TextPart extends PartBase { +export interface TextPart extends PartBase { type: "text" } -interface FileAttachmentPart extends PartBase { +export interface FileAttachmentPart extends PartBase { type: "file" path: string selection?: TextSelection @@ -34,7 +36,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => { const local = useLocal() let editorRef!: HTMLDivElement - const defaultParts = [{ type: "text", content: "" } as const] + const defaultParts = [{ type: "text", content: "", start: 0, end: 0 } as const] const [store, setStore] = createStore<{ contentParts: ContentPart[] popoverIsOpen: boolean @@ -51,7 +53,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => { event.stopPropagation() // @ts-expect-error const plainText = (event.clipboardData || window.clipboardData)?.getData("text/plain") ?? "" - addPart({ type: "text", content: plainText }) + addPart({ type: "text", content: plainText, start: 0, end: 0 }) } onMount(() => { @@ -74,7 +76,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => { key: (x) => x, onSelect: (path) => { if (!path) return - addPart({ type: "file", path, content: "@" + getFilename(path) }) + addPart({ type: "file", path, content: "@" + getFilename(path), start: 0, end: 0 }) setStore("popoverIsOpen", false) }, }) @@ -117,17 +119,26 @@ export const PromptInput: Component<PromptInputProps> = (props) => { const parseFromDOM = (): ContentPart[] => { const newParts: ContentPart[] = [] + let position = 0 editorRef.childNodes.forEach((node) => { if (node.nodeType === Node.TEXT_NODE) { - if (node.textContent) newParts.push({ type: "text", content: node.textContent }) + if (node.textContent) { + const content = node.textContent + newParts.push({ type: "text", content, start: position, end: position + content.length }) + position += content.length + } } else if (node.nodeType === Node.ELEMENT_NODE && (node as HTMLElement).dataset.type) { switch ((node as HTMLElement).dataset.type) { case "file": + const content = node.textContent! newParts.push({ type: "file", path: (node as HTMLElement).dataset.path!, - content: node.textContent!, + content, + start: position, + end: position + content.length, }) + position += content.length break default: break @@ -163,17 +174,19 @@ export const PromptInput: Component<PromptInputProps> = (props) => { const startIndex = atMatch ? atMatch.index! : cursorPosition const endIndex = atMatch ? cursorPosition : cursorPosition - const pushText = (acc: { parts: ContentPart[] }, value: string) => { + const pushText = (acc: { parts: ContentPart[]; runningIndex: number }, value: string) => { if (!value) return const last = acc.parts[acc.parts.length - 1] if (last && last.type === "text") { acc.parts[acc.parts.length - 1] = { type: "text", content: last.content + value, + start: last.start, + end: last.end + value.length, } return } - acc.parts.push({ type: "text", content: value }) + acc.parts.push({ type: "text", content: value, start: acc.runningIndex, end: acc.runningIndex + value.length }) } const { @@ -183,20 +196,20 @@ export const PromptInput: Component<PromptInputProps> = (props) => { } = store.contentParts.reduce( (acc, item) => { if (acc.inserted) { - acc.parts.push(item) + acc.parts.push({ ...item, start: acc.runningIndex, end: acc.runningIndex + item.content.length }) acc.runningIndex += item.content.length return acc } const nextIndex = acc.runningIndex + item.content.length if (nextIndex <= startIndex) { - acc.parts.push(item) + acc.parts.push({ ...item, start: acc.runningIndex, end: acc.runningIndex + item.content.length }) acc.runningIndex = nextIndex return acc } if (item.type !== "text") { - acc.parts.push(item) + acc.parts.push({ ...item, start: acc.runningIndex, end: acc.runningIndex + item.content.length }) acc.runningIndex = nextIndex return acc } @@ -207,24 +220,27 @@ export const PromptInput: Component<PromptInputProps> = (props) => { const tail = item.content.slice(tailLength) pushText(acc, head) + acc.runningIndex += head.length if (part.type === "text") { pushText(acc, part.content) + acc.runningIndex += part.content.length } if (part.type !== "text") { - acc.parts.push({ ...part }) + acc.parts.push({ ...part, start: acc.runningIndex, end: acc.runningIndex + part.content.length }) + acc.runningIndex += part.content.length } const needsGap = Boolean(atMatch) const rest = needsGap ? (tail ? (/^\s/.test(tail) ? tail : ` ${tail}`) : " ") : tail pushText(acc, rest) + acc.runningIndex += rest.length const baseCursor = startIndex + part.content.length const cursorAddition = needsGap && rest.length > 0 ? 1 : 0 acc.cursorPositionAfter = baseCursor + cursorAddition acc.inserted = true - acc.runningIndex = nextIndex return acc }, { @@ -237,9 +253,18 @@ export const PromptInput: Component<PromptInputProps> = (props) => { if (!inserted) { const baseParts = store.contentParts.filter((item) => !(item.type === "text" && item.content === "")) - const appendedAcc = { parts: [...baseParts] as ContentPart[] } - if (part.type === "text") pushText(appendedAcc, part.content) - if (part.type !== "text") appendedAcc.parts.push({ ...part }) + const runningIndex = baseParts.reduce((sum, p) => sum + p.content.length, 0) + const appendedAcc = { parts: [...baseParts] as ContentPart[], runningIndex } + if (part.type === "text") { + pushText(appendedAcc, part.content) + } + if (part.type !== "text") { + appendedAcc.parts.push({ + ...part, + start: appendedAcc.runningIndex, + end: appendedAcc.runningIndex + part.content.length, + }) + } const next = appendedAcc.parts.length > 0 ? appendedAcc.parts : defaultParts setStore("contentParts", next) setStore("popoverIsOpen", false) |
