summaryrefslogtreecommitdiffhomepage
path: root/packages/ui/src
diff options
context:
space:
mode:
authorDavid Hill <[email protected]>2025-12-16 10:54:51 +0000
committerDavid Hill <[email protected]>2025-12-16 10:54:51 +0000
commit05e0759878cb0f24c981c69ae26f6be3ea5583c6 (patch)
tree39aaf6b86a6f2b8e653120e80d8f0facb528051b /packages/ui/src
parent2330ec6dc3000ae8b86810e9d59b414ad4f05f47 (diff)
parent75e5130cf8f58b32ee3f3ba2249d5917e7e3d6fc (diff)
downloadopencode-05e0759878cb0f24c981c69ae26f6be3ea5583c6.tar.gz
opencode-05e0759878cb0f24c981c69ae26f6be3ea5583c6.zip
Merge branch 'dev' of https://github.com/sst/opencode into dev
Diffstat (limited to 'packages/ui/src')
-rw-r--r--packages/ui/src/assets/audio/nope-01.aacbin0 -> 6316 bytes
-rw-r--r--packages/ui/src/assets/audio/nope-02.aacbin0 -> 7431 bytes
-rw-r--r--packages/ui/src/assets/audio/nope-03.aacbin0 -> 6688 bytes
-rw-r--r--packages/ui/src/assets/audio/nope-04.aacbin0 -> 5573 bytes
-rw-r--r--packages/ui/src/assets/audio/nope-05.aacbin0 -> 6316 bytes
-rw-r--r--packages/ui/src/components/code.tsx2
-rw-r--r--packages/ui/src/components/dialog.tsx8
-rw-r--r--packages/ui/src/components/diff-ssr.tsx8
-rw-r--r--packages/ui/src/components/diff.css4
-rw-r--r--packages/ui/src/components/diff.tsx2
-rw-r--r--packages/ui/src/components/icon.tsx1
-rw-r--r--packages/ui/src/components/list.css1
-rw-r--r--packages/ui/src/components/list.tsx2
-rw-r--r--packages/ui/src/components/message-part.css106
-rw-r--r--packages/ui/src/components/message-part.tsx151
-rw-r--r--packages/ui/src/components/session-review.tsx2
-rw-r--r--packages/ui/src/components/session-turn.tsx30
-rw-r--r--packages/ui/src/context/data.tsx2
-rw-r--r--packages/ui/src/context/dialog.tsx162
-rw-r--r--packages/ui/src/context/marked.tsx2
-rw-r--r--packages/ui/src/custom-elements.d.ts8
-rw-r--r--packages/ui/src/pierre/index.ts22
-rw-r--r--packages/ui/src/pierre/worker.ts4
23 files changed, 390 insertions, 127 deletions
diff --git a/packages/ui/src/assets/audio/nope-01.aac b/packages/ui/src/assets/audio/nope-01.aac
new file mode 100644
index 000000000..9fb614d08
--- /dev/null
+++ b/packages/ui/src/assets/audio/nope-01.aac
Binary files differ
diff --git a/packages/ui/src/assets/audio/nope-02.aac b/packages/ui/src/assets/audio/nope-02.aac
new file mode 100644
index 000000000..75603cc16
--- /dev/null
+++ b/packages/ui/src/assets/audio/nope-02.aac
Binary files differ
diff --git a/packages/ui/src/assets/audio/nope-03.aac b/packages/ui/src/assets/audio/nope-03.aac
new file mode 100644
index 000000000..1fe459a16
--- /dev/null
+++ b/packages/ui/src/assets/audio/nope-03.aac
Binary files differ
diff --git a/packages/ui/src/assets/audio/nope-04.aac b/packages/ui/src/assets/audio/nope-04.aac
new file mode 100644
index 000000000..b731a2a07
--- /dev/null
+++ b/packages/ui/src/assets/audio/nope-04.aac
Binary files differ
diff --git a/packages/ui/src/assets/audio/nope-05.aac b/packages/ui/src/assets/audio/nope-05.aac
new file mode 100644
index 000000000..4534191b6
--- /dev/null
+++ b/packages/ui/src/assets/audio/nope-05.aac
Binary files differ
diff --git a/packages/ui/src/components/code.tsx b/packages/ui/src/components/code.tsx
index c80f0987f..77696faed 100644
--- a/packages/ui/src/components/code.tsx
+++ b/packages/ui/src/components/code.tsx
@@ -1,4 +1,4 @@
-import { type FileContents, File, FileOptions, LineAnnotation } from "@pierre/precision-diffs"
+import { type FileContents, File, FileOptions, LineAnnotation } from "@pierre/diffs"
import { ComponentProps, createEffect, createMemo, splitProps } from "solid-js"
import { createDefaultOptions, styleVariables } from "../pierre"
import { workerPool } from "../pierre/worker"
diff --git a/packages/ui/src/components/dialog.tsx b/packages/ui/src/components/dialog.tsx
index 47d6af42e..40a6ac83d 100644
--- a/packages/ui/src/components/dialog.tsx
+++ b/packages/ui/src/components/dialog.tsx
@@ -20,6 +20,14 @@ export function Dialog(props: DialogProps) {
...(props.classList ?? {}),
[props.class ?? ""]: !!props.class,
}}
+ onOpenAutoFocus={(e) => {
+ const target = e.currentTarget as HTMLElement | null
+ const autofocusEl = target?.querySelector("[autofocus]") as HTMLElement | null
+ if (autofocusEl) {
+ e.preventDefault()
+ autofocusEl.focus()
+ }
+ }}
>
<Show when={props.title || props.action}>
<div data-slot="dialog-header">
diff --git a/packages/ui/src/components/diff-ssr.tsx b/packages/ui/src/components/diff-ssr.tsx
index 800aa3730..b38b4a34f 100644
--- a/packages/ui/src/components/diff-ssr.tsx
+++ b/packages/ui/src/components/diff-ssr.tsx
@@ -1,5 +1,5 @@
-import { FileDiff } from "@pierre/precision-diffs"
-import { PreloadMultiFileDiffResult } from "@pierre/precision-diffs/ssr"
+import { FileDiff } from "@pierre/diffs"
+import { PreloadMultiFileDiffResult } from "@pierre/diffs/ssr"
import { onCleanup, onMount, Show, splitProps } from "solid-js"
import { isServer } from "solid-js/web"
import { createDefaultOptions, styleVariables, type DiffProps } from "../pierre"
@@ -65,11 +65,11 @@ export function Diff<T>(props: SSRDiffProps<T>) {
return (
<div data-component="diff" style={styleVariables} ref={container}>
- <file-diff ref={fileDiffRef} id="ssr-diff">
+ <diffs-container ref={fileDiffRef} id="ssr-diff">
<Show when={isServer}>
<template shadowrootmode="open" innerHTML={props.preloadedDiff.prerenderedHTML} />
</Show>
- </file-diff>
+ </diffs-container>
</div>
)
}
diff --git a/packages/ui/src/components/diff.css b/packages/ui/src/components/diff.css
index 690667ea7..345271a12 100644
--- a/packages/ui/src/components/diff.css
+++ b/packages/ui/src/components/diff.css
@@ -19,8 +19,8 @@
position: sticky;
background-color: var(--surface-diff-hidden-base);
color: var(--text-base);
- width: var(--pjs-column-content-width);
- left: var(--pjs-column-number-width);
+ width: var(--diffs-column-content-width);
+ left: var(--diffs-column-number-width);
padding-left: 8px;
user-select: none;
cursor: default;
diff --git a/packages/ui/src/components/diff.tsx b/packages/ui/src/components/diff.tsx
index 703043f4c..75dde0440 100644
--- a/packages/ui/src/components/diff.tsx
+++ b/packages/ui/src/components/diff.tsx
@@ -1,4 +1,4 @@
-import { FileDiff } from "@pierre/precision-diffs"
+import { FileDiff } from "@pierre/diffs"
import { createEffect, createMemo, onCleanup, splitProps } from "solid-js"
import { createDefaultOptions, type DiffProps, styleVariables } from "../pierre"
import { workerPool } from "../pierre/worker"
diff --git a/packages/ui/src/components/icon.tsx b/packages/ui/src/components/icon.tsx
index 0dbd7a650..b8e8106e8 100644
--- a/packages/ui/src/components/icon.tsx
+++ b/packages/ui/src/components/icon.tsx
@@ -51,6 +51,7 @@ const icons = {
"circle-check": `<path d="M12.4987 7.91732L8.7487 12.5007L7.08203 10.834M17.9154 10.0007C17.9154 14.3729 14.371 17.9173 9.9987 17.9173C5.62644 17.9173 2.08203 14.3729 2.08203 10.0007C2.08203 5.6284 5.62644 2.08398 9.9987 2.08398C14.371 2.08398 17.9154 5.6284 17.9154 10.0007Z" stroke="currentColor" stroke-linecap="square"/>`,
copy: `<path d="M6.2513 6.24935V2.91602H17.0846V13.7493H13.7513M13.7513 6.24935V17.0827H2.91797V6.24935H13.7513Z" stroke="currentColor" stroke-linecap="round"/>`,
check: `<path d="M5 11.9657L8.37838 14.7529L15 5.83398" stroke="currentColor" stroke-linecap="square"/>`,
+ photo: `<path d="M16.6665 16.6666L11.6665 11.6666L9.99984 13.3333L6.6665 9.99996L3.08317 13.5833M2.9165 2.91663H17.0832V17.0833H2.9165V2.91663ZM13.3332 7.49996C13.3332 8.30537 12.6803 8.95829 11.8748 8.95829C11.0694 8.95829 10.4165 8.30537 10.4165 7.49996C10.4165 6.69454 11.0694 6.04163 11.8748 6.04163C12.6803 6.04163 13.3332 6.69454 13.3332 7.49996Z" stroke="currentColor" stroke-linecap="square"/>`,
}
export interface IconProps extends ComponentProps<"svg"> {
diff --git a/packages/ui/src/components/list.css b/packages/ui/src/components/list.css
index cd9e73d1d..368065e53 100644
--- a/packages/ui/src/components/list.css
+++ b/packages/ui/src/components/list.css
@@ -112,6 +112,7 @@
padding: 4px 10px;
align-items: center;
color: var(--text-strong);
+ scroll-margin-top: 28px;
/* text-14-medium */
font-family: var(--font-family-sans);
diff --git a/packages/ui/src/components/list.tsx b/packages/ui/src/components/list.tsx
index 7ec6e159d..0ed745f32 100644
--- a/packages/ui/src/components/list.tsx
+++ b/packages/ui/src/components/list.tsx
@@ -79,7 +79,7 @@ export function List<T>(props: ListProps<T> & { ref?: (ref: ListRef) => void })
return
}
const element = scrollRef()?.querySelector(`[data-key="${active()}"]`)
- element?.scrollIntoView({ block: "nearest", behavior: "smooth" })
+ element?.scrollIntoView({ block: "center", behavior: "smooth" })
})
const handleSelect = (item: T | undefined, index: number) => {
diff --git a/packages/ui/src/components/message-part.css b/packages/ui/src/components/message-part.css
index e369f9220..01f34ceff 100644
--- a/packages/ui/src/components/message-part.css
+++ b/packages/ui/src/components/message-part.css
@@ -14,11 +14,78 @@
line-height: var(--line-height-large);
letter-spacing: var(--letter-spacing-normal);
color: var(--text-base);
- display: -webkit-box;
- line-clamp: 3;
- -webkit-line-clamp: 3;
- -webkit-box-orient: vertical;
- overflow: hidden;
+ display: flex;
+ flex-direction: column;
+ gap: 8px;
+
+ [data-slot="user-message-attachments"] {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 8px;
+ }
+
+ [data-slot="user-message-attachment"] {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ border-radius: 6px;
+ overflow: hidden;
+ background: var(--surface-base);
+ border: 1px solid var(--border-base);
+ transition: border-color 0.15s ease;
+
+ &:hover {
+ border-color: var(--border-strong-base);
+ }
+
+ &[data-type="image"] {
+ width: 48px;
+ height: 48px;
+ }
+
+ &[data-type="file"] {
+ width: 48px;
+ height: 48px;
+ }
+ }
+
+ [data-slot="user-message-attachment-image"] {
+ width: 100%;
+ height: 100%;
+ object-fit: cover;
+ }
+
+ [data-slot="user-message-attachment-icon"] {
+ width: 100%;
+ height: 100%;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ color: var(--icon-weak);
+
+ [data-component="icon"] {
+ width: 20px;
+ height: 20px;
+ }
+ }
+
+ [data-slot="user-message-text"] {
+ display: -webkit-box;
+ white-space: pre-wrap;
+ line-clamp: 3;
+ -webkit-line-clamp: 3;
+ -webkit-box-orient: vertical;
+ overflow: hidden;
+ }
+
+ .text-text-strong {
+ color: var(--text-strong);
+ }
+
+ .font-medium {
+ font-weight: var(--font-weight-medium);
+ }
}
[data-component="text-part"] {
@@ -108,15 +175,19 @@
display: flex;
align-items: center;
justify-content: space-between;
+ gap: 8px;
width: 100%;
[data-slot="message-part-title-area"] {
+ flex-grow: 1;
display: flex;
align-items: center;
gap: 8px;
+ min-width: 0;
}
[data-slot="message-part-title"] {
+ flex-shrink: 0;
font-family: var(--font-family-sans);
font-size: var(--font-size-base);
font-style: normal;
@@ -129,14 +200,22 @@
[data-slot="message-part-path"] {
display: flex;
+ flex-grow: 1;
+ min-width: 0;
}
[data-slot="message-part-directory"] {
color: var(--text-weak);
+ text-overflow: ellipsis;
+ overflow: hidden;
+ white-space: nowrap;
+ direction: rtl;
+ text-align: left;
}
[data-slot="message-part-filename"] {
color: var(--text-strong);
+ flex-shrink: 0;
}
[data-slot="message-part-actions"] {
@@ -151,6 +230,23 @@
border-top: 1px solid var(--border-weaker-base);
}
+[data-component="write-content"] {
+ border-top: 1px solid var(--border-weaker-base);
+ max-height: 240px;
+ overflow-y: auto;
+
+ [data-component="code"] {
+ padding-bottom: 0px !important;
+ }
+
+ /* Hide scrollbar */
+ scrollbar-width: none;
+ -ms-overflow-style: none;
+ &::-webkit-scrollbar {
+ display: none;
+ }
+}
+
[data-component="tool-action"] {
width: 24px;
height: 24px;
diff --git a/packages/ui/src/components/message-part.tsx b/packages/ui/src/components/message-part.tsx
index f00c43bd8..33b519ea4 100644
--- a/packages/ui/src/components/message-part.tsx
+++ b/packages/ui/src/components/message-part.tsx
@@ -2,6 +2,7 @@ import { Component, createMemo, For, Match, Show, Switch } from "solid-js"
import { Dynamic } from "solid-js/web"
import {
AssistantMessage,
+ FilePart,
Message as MessageType,
Part as PartType,
TextPart,
@@ -74,13 +75,93 @@ export function AssistantMessageDisplay(props: { message: AssistantMessage; part
}
export function UserMessageDisplay(props: { message: UserMessage; parts: PartType[] }) {
- const text = createMemo(() =>
- props.parts
- ?.filter((p) => p.type === "text" && !(p as TextPart).synthetic)
- ?.map((p) => (p as TextPart).text)
- ?.join(""),
+ const textPart = createMemo(
+ () => props.parts?.find((p) => p.type === "text" && !(p as TextPart).synthetic) as TextPart | undefined,
+ )
+
+ const text = createMemo(() => textPart()?.text || "")
+
+ const files = createMemo(() => (props.parts?.filter((p) => p.type === "file") as FilePart[]) ?? [])
+
+ const attachments = createMemo(() =>
+ files()?.filter((f) => {
+ const mime = f.mime
+ return mime.startsWith("image/") || mime === "application/pdf"
+ }),
+ )
+
+ const inlineFiles = createMemo(() =>
+ files().filter((f) => {
+ const mime = f.mime
+ return !mime.startsWith("image/") && mime !== "application/pdf" && f.source?.text?.start !== undefined
+ }),
+ )
+
+ return (
+ <div data-component="user-message">
+ <Show when={attachments().length > 0}>
+ <div data-slot="user-message-attachments">
+ <For each={attachments()}>
+ {(file) => (
+ <div data-slot="user-message-attachment" data-type={file.mime.startsWith("image/") ? "image" : "file"}>
+ <Show
+ when={file.mime.startsWith("image/") && file.url}
+ fallback={
+ <div data-slot="user-message-attachment-icon">
+ <Icon name="folder" />
+ </div>
+ }
+ >
+ <img data-slot="user-message-attachment-image" src={file.url} alt={file.filename ?? "attachment"} />
+ </Show>
+ </div>
+ )}
+ </For>
+ </div>
+ </Show>
+ <Show when={text()}>
+ <div data-slot="user-message-text">
+ <HighlightedText text={text()} references={inlineFiles()} />
+ </div>
+ </Show>
+ </div>
+ )
+}
+
+function HighlightedText(props: { text: string; references: FilePart[] }) {
+ const segments = createMemo(() => {
+ const text = props.text
+ const refs = [...props.references].sort((a, b) => (a.source?.text?.start ?? 0) - (b.source?.text?.start ?? 0))
+
+ const result: { text: string; highlight?: boolean }[] = []
+ let lastIndex = 0
+
+ for (const ref of refs) {
+ const start = ref.source?.text?.start
+ const end = ref.source?.text?.end
+
+ if (start === undefined || end === undefined || start < lastIndex) continue
+
+ if (start > lastIndex) {
+ result.push({ text: text.slice(lastIndex, start) })
+ }
+
+ result.push({ text: text.slice(start, end), highlight: true })
+ lastIndex = end
+ }
+
+ if (lastIndex < text.length) {
+ result.push({ text: text.slice(lastIndex) })
+ }
+
+ return result
+ })
+
+ return (
+ <For each={segments()}>
+ {(segment) => <span classList={{ "text-text-strong font-medium": segment.highlight }}>{segment.text}</span>}
+ </For>
)
- return <div data-component="user-message">{text()}</div>
}
export function Part(props: MessagePartProps) {
@@ -220,8 +301,12 @@ ToolRegistry.register({
render(props) {
return (
<BasicTool icon="bullet-list" trigger={{ title: "List", subtitle: getDirectory(props.input.path || "/") }}>
- <Show when={false && props.output}>
- <div data-component="tool-output">{props.output}</div>
+ <Show when={props.output}>
+ {(output) => (
+ <div data-component="tool-output" data-scrollable>
+ <Markdown text={output()} />
+ </div>
+ )}
</Show>
</BasicTool>
)
@@ -240,8 +325,12 @@ ToolRegistry.register({
args: props.input.pattern ? ["pattern=" + props.input.pattern] : [],
}}
>
- <Show when={false && props.output}>
- <div data-component="tool-output">{props.output}</div>
+ <Show when={props.output}>
+ {(output) => (
+ <div data-component="tool-output" data-scrollable>
+ <Markdown text={output()} />
+ </div>
+ )}
</Show>
</BasicTool>
)
@@ -263,8 +352,12 @@ ToolRegistry.register({
args,
}}
>
- <Show when={false && props.output}>
- <div data-component="tool-output">{props.output}</div>
+ <Show when={props.output}>
+ {(output) => (
+ <div data-component="tool-output" data-scrollable>
+ <Markdown text={output()} />
+ </div>
+ )}
</Show>
</BasicTool>
)
@@ -288,8 +381,12 @@ ToolRegistry.register({
),
}}
>
- <Show when={false && props.output}>
- <div data-component="tool-output">{props.output}</div>
+ <Show when={props.output}>
+ {(output) => (
+ <div data-component="tool-output" data-scrollable>
+ <Markdown text={output()} />
+ </div>
+ )}
</Show>
</BasicTool>
)
@@ -308,8 +405,12 @@ ToolRegistry.register({
subtitle: props.input.description,
}}
>
- <Show when={false && props.output}>
- <div data-component="tool-output">{props.output}</div>
+ <Show when={props.output}>
+ {(output) => (
+ <div data-component="tool-output" data-scrollable>
+ <Markdown text={output()} />
+ </div>
+ )}
</Show>
</BasicTool>
)
@@ -387,6 +488,7 @@ ToolRegistry.register({
ToolRegistry.register({
name: "write",
render(props) {
+ console.log(props)
return (
<BasicTool
icon="code-lines"
@@ -405,9 +507,19 @@ ToolRegistry.register({
</div>
}
>
- <Show when={false && props.output}>
- <div data-component="tool-output">{props.output}</div>
- </Show>
+ {/* <Show when={props.input.content}> */}
+ {/* <div data-component="write-content"> */}
+ {/* <Code */}
+ {/* file={{ */}
+ {/* name: props.input.filePath, */}
+ {/* contents: props.input.content, */}
+ {/* cacheKey: checksum(props.input.content), */}
+ {/* }} */}
+ {/* overflow="scroll" */}
+ {/* class="pb-40" */}
+ {/* /> */}
+ {/* </div> */}
+ {/* </Show> */}
</BasicTool>
)
},
@@ -418,6 +530,7 @@ ToolRegistry.register({
render(props) {
return (
<BasicTool
+ defaultOpen
icon="checklist"
trigger={{
title: "To-dos",
diff --git a/packages/ui/src/components/session-review.tsx b/packages/ui/src/components/session-review.tsx
index 8009091b7..b47ab55b1 100644
--- a/packages/ui/src/components/session-review.tsx
+++ b/packages/ui/src/components/session-review.tsx
@@ -9,7 +9,7 @@ import { getDirectory, getFilename } from "@opencode-ai/util/path"
import { For, Match, Show, Switch, type JSX } from "solid-js"
import { createStore } from "solid-js/store"
import { type FileDiff } from "@opencode-ai/sdk/v2"
-import { PreloadMultiFileDiffResult } from "@pierre/precision-diffs/ssr"
+import { PreloadMultiFileDiffResult } from "@pierre/diffs/ssr"
import { Dynamic } from "solid-js/web"
import { checksum } from "@opencode-ai/util/encode"
diff --git a/packages/ui/src/components/session-turn.tsx b/packages/ui/src/components/session-turn.tsx
index ad2e6c36e..0f5a26a2a 100644
--- a/packages/ui/src/components/session-turn.tsx
+++ b/packages/ui/src/components/session-turn.tsx
@@ -24,6 +24,8 @@ export function SessionTurn(
props: ParentProps<{
sessionID: string
messageID: string
+ stepsExpanded?: boolean
+ onStepsExpandedChange?: (expanded: boolean) => void
classes?: {
root?: string
content?: string
@@ -60,11 +62,12 @@ export function SessionTurn(
function handleScroll() {
if (!scrollRef) return
- // prevents scroll loops
- if (working() && scrollRef.scrollTop < 100) return
- setState("scrollY", scrollRef.scrollTop)
if (state.autoScrolling) return
const { scrollTop, scrollHeight, clientHeight } = scrollRef
+ const scrollRoom = scrollHeight - clientHeight
+ if (scrollRoom > 100) {
+ setState("scrollY", scrollTop)
+ }
const atBottom = scrollHeight - scrollTop - clientHeight < 50
if (!atBottom && working()) {
setState("userScrolled", true)
@@ -222,11 +225,17 @@ export function SessionTurn(
const [store, setStore] = createStore({
status: rawStatus(),
- stepsExpanded: true,
+ stepsExpanded: props.stepsExpanded ?? working(),
duration: duration(),
})
createEffect(() => {
+ if (props.stepsExpanded !== undefined) {
+ setStore("stepsExpanded", props.stepsExpanded)
+ }
+ })
+
+ createEffect(() => {
const timer = setInterval(() => {
setStore("duration", duration())
}, 1000)
@@ -260,8 +269,13 @@ export function SessionTurn(
createEffect((prev) => {
const isWorking = working()
+ if (!prev && isWorking) {
+ setStore("stepsExpanded", true)
+ props.onStepsExpandedChange?.(true)
+ }
if (prev && !isWorking && !state.userScrolled) {
setStore("stepsExpanded", false)
+ props.onStepsExpandedChange?.(false)
}
return isWorking
}, working())
@@ -278,7 +292,7 @@ export function SessionTurn(
<div data-slot="session-turn-message-header">
<div data-slot="session-turn-message-title">
<Switch>
- <Match when={working()}>
+ <Match when={working() && message().id === userMessages().at(-1)?.id}>
<Typewriter as="h1" text={message().summary?.title} data-slot="session-turn-typewriter" />
</Match>
<Match when={true}>
@@ -298,7 +312,11 @@ export function SessionTurn(
data-slot="session-turn-collapsible-trigger-content"
variant="ghost"
size="small"
- onClick={() => setStore("stepsExpanded", !store.stepsExpanded)}
+ onClick={() => {
+ const next = !store.stepsExpanded
+ setStore("stepsExpanded", next)
+ props.onStepsExpandedChange?.(next)
+ }}
>
<Show when={working()}>
<Spinner />
diff --git a/packages/ui/src/context/data.tsx b/packages/ui/src/context/data.tsx
index 265178e10..16efe7779 100644
--- a/packages/ui/src/context/data.tsx
+++ b/packages/ui/src/context/data.tsx
@@ -1,6 +1,6 @@
import type { Message, Session, Part, FileDiff, SessionStatus } from "@opencode-ai/sdk/v2"
import { createSimpleContext } from "./helper"
-import { PreloadMultiFileDiffResult } from "@pierre/precision-diffs/ssr"
+import { PreloadMultiFileDiffResult } from "@pierre/diffs/ssr"
type Data = {
session: Session[]
diff --git a/packages/ui/src/context/dialog.tsx b/packages/ui/src/context/dialog.tsx
index af5da06f9..71fc63806 100644
--- a/packages/ui/src/context/dialog.tsx
+++ b/packages/ui/src/context/dialog.tsx
@@ -1,79 +1,105 @@
-import { For, Show, type JSX } from "solid-js"
-import { createStore } from "solid-js/store"
-import { createSimpleContext } from "@opencode-ai/ui/context"
+import {
+ createContext,
+ createEffect,
+ createSignal,
+ getOwner,
+ Owner,
+ ParentProps,
+ runWithOwner,
+ Show,
+ useContext,
+ type JSX,
+} from "solid-js"
+import { Dialog as Kobalte } from "@kobalte/core/dialog"
+
+type DialogElement = () => JSX.Element
-type DialogElement = JSX.Element | (() => JSX.Element)
+const Context = createContext<ReturnType<typeof init>>()
-export const { use: useDialog, provider: DialogProvider } = createSimpleContext({
- name: "Dialog",
- init: () => {
- const [store, setStore] = createStore({
- stack: [] as {
+function init() {
+ const [active, setActive] = createSignal<
+ | {
+ id: string
element: DialogElement
onClose?: () => void
- }[],
- })
+ owner: Owner
+ }
+ | undefined
+ >()
- return {
- get stack() {
- return store.stack
- },
- push(element: DialogElement, onClose?: () => void) {
- setStore("stack", (s) => [...s, { element, onClose }])
- },
- pop() {
- const current = store.stack.at(-1)
- current?.onClose?.()
- setStore("stack", store.stack.slice(0, -1))
- },
- replace(element: DialogElement, onClose?: () => void) {
- for (const item of store.stack) {
- item.onClose?.()
- }
- setStore("stack", [{ element, onClose }])
- },
- clear() {
- for (const item of store.stack) {
- item.onClose?.()
- }
- setStore("stack", [])
- },
- }
- },
-})
+ const result = {
+ get active() {
+ return active()
+ },
+ close() {
+ active()?.onClose?.()
+ setActive(undefined)
+ },
+ show(element: DialogElement, owner: Owner, onClose?: () => void) {
+ active()?.onClose?.()
+ const id = Math.random().toString(36).slice(2)
+ setActive({
+ id,
+ element: () =>
+ runWithOwner(owner, () => (
+ <Show when={active()?.id === id}>
+ <Kobalte
+ modal
+ open={true}
+ onOpenChange={(open) => {
+ if (!open) {
+ console.log("closing")
+ result.close()
+ }
+ }}
+ >
+ <Kobalte.Portal>
+ <Kobalte.Overlay data-component="dialog-overlay" />
+ {element()}
+ </Kobalte.Portal>
+ </Kobalte>
+ </Show>
+ )),
+ onClose,
+ owner,
+ })
+ },
+ }
-import { Dialog as Kobalte } from "@kobalte/core/dialog"
+ return result
+}
-export function DialogRoot(props: { children?: JSX.Element }) {
- const dialog = useDialog()
+export function DialogProvider(props: ParentProps) {
+ const ctx = init()
+ createEffect(() => {
+ console.log("active", ctx.active)
+ })
return (
- <>
+ <Context.Provider value={ctx}>
{props.children}
- <Show when={dialog.stack.length > 0}>
- <div data-component="dialog-stack">
- <For each={dialog.stack}>
- {(item, index) => (
- <Show when={index() === dialog.stack.length - 1}>
- <Kobalte
- modal
- defaultOpen
- onOpenChange={(open) => {
- if (!open) {
- item.onClose?.()
- dialog.pop()
- }
- }}
- >
- <Kobalte.Portal>
- <Kobalte.Overlay data-component="dialog-overlay" />
- {typeof item.element === "function" ? item.element() : item.element}
- </Kobalte.Portal>
- </Kobalte>
- </Show>
- )}
- </For>
- </div>
- </Show>
- </>
+ <div data-component="dialog-stack">{ctx.active?.element?.()}</div>
+ </Context.Provider>
)
}
+
+export function useDialog() {
+ const ctx = useContext(Context)
+ const owner = getOwner()
+ if (!owner) {
+ throw new Error("useDialog must be used within a DialogProvider")
+ }
+ if (!ctx) {
+ throw new Error("useDialog must be used within a DialogProvider")
+ }
+ return {
+ get active() {
+ return ctx.active
+ },
+ show(element: DialogElement, onClose?: () => void) {
+ ctx.show(element, owner, onClose)
+ },
+ close() {
+ ctx.close()
+ },
+ }
+}
diff --git a/packages/ui/src/context/marked.tsx b/packages/ui/src/context/marked.tsx
index 0d9c44758..f4d85519d 100644
--- a/packages/ui/src/context/marked.tsx
+++ b/packages/ui/src/context/marked.tsx
@@ -2,7 +2,7 @@ import { marked } from "marked"
import markedShiki from "marked-shiki"
import { bundledLanguages, type BundledLanguage } from "shiki"
import { createSimpleContext } from "./helper"
-import { getSharedHighlighter, registerCustomTheme, ThemeRegistrationResolved } from "@pierre/precision-diffs"
+import { getSharedHighlighter, registerCustomTheme, ThemeRegistrationResolved } from "@pierre/diffs"
registerCustomTheme("OpenCode", () => {
return Promise.resolve({
diff --git a/packages/ui/src/custom-elements.d.ts b/packages/ui/src/custom-elements.d.ts
index 6ad3ea34e..b756e51da 100644
--- a/packages/ui/src/custom-elements.d.ts
+++ b/packages/ui/src/custom-elements.d.ts
@@ -1,12 +1,12 @@
/**
- * TypeScript declaration for the <file-diff> custom element.
- * This tells TypeScript that <file-diff> is a valid JSX element in SolidJS.
- * Required for using the precision-diffs web component in .tsx files.
+ * TypeScript declaration for the <diffs-container> custom element.
+ * This tells TypeScript that <diffs-container> is a valid JSX element in SolidJS.
+ * Required for using the @pierre/diffs web component in .tsx files.
*/
declare module "solid-js" {
namespace JSX {
interface IntrinsicElements {
- "file-diff": HTMLAttributes<HTMLElement>
+ "diffs-container": HTMLAttributes<HTMLElement>
}
}
}
diff --git a/packages/ui/src/pierre/index.ts b/packages/ui/src/pierre/index.ts
index 8780bc6c5..f83fc82a2 100644
--- a/packages/ui/src/pierre/index.ts
+++ b/packages/ui/src/pierre/index.ts
@@ -1,4 +1,4 @@
-import { DiffLineAnnotation, FileContents, FileDiffOptions } from "@pierre/precision-diffs"
+import { DiffLineAnnotation, FileContents, FileDiffOptions } from "@pierre/diffs"
import { ComponentProps } from "solid-js"
export type DiffProps<T = {}> = FileDiffOptions<T> & {
@@ -10,8 +10,8 @@ export type DiffProps<T = {}> = FileDiffOptions<T> & {
}
const unsafeCSS = `
-[data-pjs-header],
-[data-pjs] {
+[data-diffs-header],
+[data-diffs] {
[data-separator-wrapper] {
margin: 0 !important;
border-radius: 0 !important;
@@ -71,12 +71,12 @@ export function createDefaultOptions<T>(style: FileDiffOptions<T>["diffStyle"])
}
export const styleVariables = {
- "--pjs-font-family": "var(--font-family-mono)",
- "--pjs-font-size": "var(--font-size-small)",
- "--pjs-line-height": "24px",
- "--pjs-tab-size": 2,
- "--pjs-font-features": "var(--font-family-mono--font-feature-settings)",
- "--pjs-header-font-family": "var(--font-family-sans)",
- "--pjs-gap-block": 0,
- "--pjs-min-number-column-width": "4ch",
+ "--diffs-font-family": "var(--font-family-mono)",
+ "--diffs-font-size": "var(--font-size-small)",
+ "--diffs-line-height": "24px",
+ "--diffs-tab-size": 2,
+ "--diffs-font-features": "var(--font-family-mono--font-feature-settings)",
+ "--diffs-header-font-family": "var(--font-family-sans)",
+ "--diffs-gap-block": 0,
+ "--diffs-min-number-column-width": "4ch",
}
diff --git a/packages/ui/src/pierre/worker.ts b/packages/ui/src/pierre/worker.ts
index 2b2da1f09..e47268d4e 100644
--- a/packages/ui/src/pierre/worker.ts
+++ b/packages/ui/src/pierre/worker.ts
@@ -1,5 +1,5 @@
-import { getOrCreateWorkerPoolSingleton } from "@pierre/precision-diffs/worker"
-import ShikiWorkerUrl from "@pierre/precision-diffs/worker/worker.js?worker&url"
+import { getOrCreateWorkerPoolSingleton } from "@pierre/diffs/worker"
+import ShikiWorkerUrl from "@pierre/diffs/worker/worker.js?worker&url"
export function workerFactory(): Worker {
return new Worker(ShikiWorkerUrl, { type: "module" })