refactor: simplify solid reactivity across app and web (#20497)
parent
db93891373
commit
d540d363a7
|
|
@ -329,10 +329,9 @@ export default function Page() {
|
||||||
const { params, sessionKey, tabs, view } = useSessionLayout()
|
const { params, sessionKey, tabs, view } = useSessionLayout()
|
||||||
|
|
||||||
createEffect(() => {
|
createEffect(() => {
|
||||||
if (!untrack(() => prompt.ready())) return
|
if (!prompt.ready()) return
|
||||||
prompt.ready()
|
|
||||||
untrack(() => {
|
untrack(() => {
|
||||||
if (params.id || !prompt.ready()) return
|
if (params.id) return
|
||||||
const text = searchParams.prompt
|
const text = searchParams.prompt
|
||||||
if (!text) return
|
if (!text) return
|
||||||
prompt.set([{ type: "text", content: text, start: 0, end: text.length }], text.length)
|
prompt.set([{ type: "text", content: text, start: 0, end: text.length }], text.length)
|
||||||
|
|
|
||||||
|
|
@ -294,11 +294,6 @@ export function createLineCommentState<T>(props: LineCommentStateProps<T>) {
|
||||||
cancelDraft()
|
cancelDraft()
|
||||||
}
|
}
|
||||||
|
|
||||||
createEffect(() => {
|
|
||||||
props.commenting()
|
|
||||||
setDraft("")
|
|
||||||
})
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
draft,
|
draft,
|
||||||
setDraft,
|
setDraft,
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import { useFilteredList } from "@opencode-ai/ui/hooks"
|
import { useFilteredList } from "@opencode-ai/ui/hooks"
|
||||||
import { getDirectory, getFilename } from "@opencode-ai/util/path"
|
import { getDirectory, getFilename } from "@opencode-ai/util/path"
|
||||||
import { createEffect, createSignal, For, onMount, Show, splitProps, type JSX } from "solid-js"
|
import { createSignal, For, onMount, Show, splitProps, type JSX } from "solid-js"
|
||||||
import { Button } from "./button"
|
import { Button } from "./button"
|
||||||
import { FileIcon } from "./file-icon"
|
import { FileIcon } from "./file-icon"
|
||||||
import { Icon } from "./icon"
|
import { Icon } from "./icon"
|
||||||
|
|
@ -210,7 +210,6 @@ export const LineCommentEditor = (props: LineCommentEditorProps) => {
|
||||||
const refs = {
|
const refs = {
|
||||||
textarea: undefined as HTMLTextAreaElement | undefined,
|
textarea: undefined as HTMLTextAreaElement | undefined,
|
||||||
}
|
}
|
||||||
const [text, setText] = createSignal(split.value)
|
|
||||||
const [open, setOpen] = createSignal(false)
|
const [open, setOpen] = createSignal(false)
|
||||||
|
|
||||||
function selectMention(item: { path: string } | undefined) {
|
function selectMention(item: { path: string } | undefined) {
|
||||||
|
|
@ -220,10 +219,9 @@ export const LineCommentEditor = (props: LineCommentEditorProps) => {
|
||||||
const query = currentMention()
|
const query = currentMention()
|
||||||
if (!textarea || !query) return
|
if (!textarea || !query) return
|
||||||
|
|
||||||
const value = `${text().slice(0, query.start)}@${item.path} ${text().slice(query.end)}`
|
const value = `${textarea.value.slice(0, query.start)}@${item.path} ${textarea.value.slice(query.end)}`
|
||||||
const cursor = query.start + item.path.length + 2
|
const cursor = query.start + item.path.length + 2
|
||||||
|
|
||||||
setText(value)
|
|
||||||
split.onInput(value)
|
split.onInput(value)
|
||||||
closeMention()
|
closeMention()
|
||||||
|
|
||||||
|
|
@ -257,10 +255,6 @@ export const LineCommentEditor = (props: LineCommentEditorProps) => {
|
||||||
fn()
|
fn()
|
||||||
}
|
}
|
||||||
|
|
||||||
createEffect(() => {
|
|
||||||
setText(split.value)
|
|
||||||
})
|
|
||||||
|
|
||||||
const closeMention = () => {
|
const closeMention = () => {
|
||||||
setOpen(false)
|
setOpen(false)
|
||||||
mention.clear()
|
mention.clear()
|
||||||
|
|
@ -302,7 +296,7 @@ export const LineCommentEditor = (props: LineCommentEditorProps) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
const submit = () => {
|
const submit = () => {
|
||||||
const value = text().trim()
|
const value = split.value.trim()
|
||||||
if (!value) return
|
if (!value) return
|
||||||
split.onSubmit(value)
|
split.onSubmit(value)
|
||||||
}
|
}
|
||||||
|
|
@ -322,10 +316,9 @@ export const LineCommentEditor = (props: LineCommentEditorProps) => {
|
||||||
data-slot="line-comment-textarea"
|
data-slot="line-comment-textarea"
|
||||||
rows={split.rows ?? 3}
|
rows={split.rows ?? 3}
|
||||||
placeholder={split.placeholder ?? i18n.t("ui.lineComment.placeholder")}
|
placeholder={split.placeholder ?? i18n.t("ui.lineComment.placeholder")}
|
||||||
value={text()}
|
value={split.value}
|
||||||
on:input={(e) => {
|
on:input={(e) => {
|
||||||
const value = (e.currentTarget as HTMLTextAreaElement).value
|
const value = (e.currentTarget as HTMLTextAreaElement).value
|
||||||
setText(value)
|
|
||||||
split.onInput(value)
|
split.onInput(value)
|
||||||
syncMention()
|
syncMention()
|
||||||
}}
|
}}
|
||||||
|
|
@ -422,7 +415,7 @@ export const LineCommentEditor = (props: LineCommentEditorProps) => {
|
||||||
type="button"
|
type="button"
|
||||||
data-slot="line-comment-action"
|
data-slot="line-comment-action"
|
||||||
data-variant="primary"
|
data-variant="primary"
|
||||||
disabled={text().trim().length === 0}
|
disabled={split.value.trim().length === 0}
|
||||||
on:mousedown={hold as any}
|
on:mousedown={hold as any}
|
||||||
on:click={click(submit) as any}
|
on:click={click(submit) as any}
|
||||||
>
|
>
|
||||||
|
|
@ -434,7 +427,7 @@ export const LineCommentEditor = (props: LineCommentEditorProps) => {
|
||||||
<Button size="small" variant="ghost" onClick={split.onCancel}>
|
<Button size="small" variant="ghost" onClick={split.onCancel}>
|
||||||
{split.cancelLabel ?? i18n.t("ui.common.cancel")}
|
{split.cancelLabel ?? i18n.t("ui.common.cancel")}
|
||||||
</Button>
|
</Button>
|
||||||
<Button size="small" variant="primary" disabled={text().trim().length === 0} onClick={submit}>
|
<Button size="small" variant="primary" disabled={split.value.trim().length === 0} onClick={submit}>
|
||||||
{split.submitLabel ?? i18n.t("ui.lineComment.submit")}
|
{split.submitLabel ?? i18n.t("ui.lineComment.submit")}
|
||||||
</Button>
|
</Button>
|
||||||
</Show>
|
</Show>
|
||||||
|
|
|
||||||
|
|
@ -230,6 +230,19 @@ function createPacedValue(getValue: () => string, live?: () => boolean) {
|
||||||
return value
|
return value
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function PacedMarkdown(props: { text: string; cacheKey: string; streaming: boolean }) {
|
||||||
|
const value = createPacedValue(
|
||||||
|
() => props.text,
|
||||||
|
() => props.streaming,
|
||||||
|
)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Show when={value()}>
|
||||||
|
<Markdown text={value()} cacheKey={props.cacheKey} streaming={props.streaming} />
|
||||||
|
</Show>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
function relativizeProjectPath(path: string, directory?: string) {
|
function relativizeProjectPath(path: string, directory?: string) {
|
||||||
if (!path) return ""
|
if (!path) return ""
|
||||||
if (!directory) return path
|
if (!directory) return path
|
||||||
|
|
@ -1373,8 +1386,7 @@ PART_MAPPING["text"] = function TextPartDisplay(props) {
|
||||||
const streaming = createMemo(
|
const streaming = createMemo(
|
||||||
() => props.message.role === "assistant" && typeof (props.message as AssistantMessage).time.completed !== "number",
|
() => props.message.role === "assistant" && typeof (props.message as AssistantMessage).time.completed !== "number",
|
||||||
)
|
)
|
||||||
const displayText = () => (part().text ?? "").trim()
|
const text = () => (part().text ?? "").trim()
|
||||||
const throttledText = createPacedValue(displayText, streaming)
|
|
||||||
const isLastTextPart = createMemo(() => {
|
const isLastTextPart = createMemo(() => {
|
||||||
const last = (data.store.part?.[props.message.id] ?? [])
|
const last = (data.store.part?.[props.message.id] ?? [])
|
||||||
.filter((item): item is TextPart => item?.type === "text" && !!item.text?.trim())
|
.filter((item): item is TextPart => item?.type === "text" && !!item.text?.trim())
|
||||||
|
|
@ -1390,7 +1402,7 @@ PART_MAPPING["text"] = function TextPartDisplay(props) {
|
||||||
const [copied, setCopied] = createSignal(false)
|
const [copied, setCopied] = createSignal(false)
|
||||||
|
|
||||||
const handleCopy = async () => {
|
const handleCopy = async () => {
|
||||||
const content = displayText()
|
const content = text()
|
||||||
if (!content) return
|
if (!content) return
|
||||||
await navigator.clipboard.writeText(content)
|
await navigator.clipboard.writeText(content)
|
||||||
setCopied(true)
|
setCopied(true)
|
||||||
|
|
@ -1398,10 +1410,12 @@ PART_MAPPING["text"] = function TextPartDisplay(props) {
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Show when={throttledText()}>
|
<Show when={text()}>
|
||||||
<div data-component="text-part">
|
<div data-component="text-part">
|
||||||
<div data-slot="text-part-body">
|
<div data-slot="text-part-body">
|
||||||
<Markdown text={throttledText()} cacheKey={part().id} streaming={streaming()} />
|
<Show when={streaming()} fallback={<Markdown text={text()} cacheKey={part().id} streaming={false} />}>
|
||||||
|
<PacedMarkdown text={text()} cacheKey={part().id} streaming={streaming()} />
|
||||||
|
</Show>
|
||||||
</div>
|
</div>
|
||||||
<Show when={showCopy()}>
|
<Show when={showCopy()}>
|
||||||
<div data-slot="text-part-copy-wrapper" data-interrupted={interrupted() ? "" : undefined}>
|
<div data-slot="text-part-copy-wrapper" data-interrupted={interrupted() ? "" : undefined}>
|
||||||
|
|
@ -1437,12 +1451,13 @@ PART_MAPPING["reasoning"] = function ReasoningPartDisplay(props) {
|
||||||
() => props.message.role === "assistant" && typeof (props.message as AssistantMessage).time.completed !== "number",
|
() => props.message.role === "assistant" && typeof (props.message as AssistantMessage).time.completed !== "number",
|
||||||
)
|
)
|
||||||
const text = () => part().text.trim()
|
const text = () => part().text.trim()
|
||||||
const throttledText = createPacedValue(text, streaming)
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Show when={throttledText()}>
|
<Show when={text()}>
|
||||||
<div data-component="reasoning-part">
|
<div data-component="reasoning-part">
|
||||||
<Markdown text={throttledText()} cacheKey={part().id} streaming={streaming()} />
|
<Show when={streaming()} fallback={<Markdown text={text()} cacheKey={part().id} streaming={false} />}>
|
||||||
|
<PacedMarkdown text={text()} cacheKey={part().id} streaming={streaming()} />
|
||||||
|
</Show>
|
||||||
</div>
|
</div>
|
||||||
</Show>
|
</Show>
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -343,14 +343,12 @@ export function SessionTurn(
|
||||||
})
|
})
|
||||||
const assistantDerived = createMemo(() => {
|
const assistantDerived = createMemo(() => {
|
||||||
let visible = 0
|
let visible = 0
|
||||||
let tail: "text" | "other" | undefined
|
|
||||||
let reason: string | undefined
|
let reason: string | undefined
|
||||||
const show = showReasoningSummaries()
|
const show = showReasoningSummaries()
|
||||||
for (const message of assistantMessages()) {
|
for (const message of assistantMessages()) {
|
||||||
for (const part of list(data.store.part?.[message.id], emptyParts)) {
|
for (const part of list(data.store.part?.[message.id], emptyParts)) {
|
||||||
if (partState(part, show) === "visible") {
|
if (partState(part, show) === "visible") {
|
||||||
visible++
|
visible++
|
||||||
tail = part.type === "text" ? "text" : "other"
|
|
||||||
}
|
}
|
||||||
if (part.type === "reasoning" && part.text) {
|
if (part.type === "reasoning" && part.text) {
|
||||||
const h = heading(part.text)
|
const h = heading(part.text)
|
||||||
|
|
@ -358,10 +356,9 @@ export function SessionTurn(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return { visible, tail, reason }
|
return { visible, reason }
|
||||||
})
|
})
|
||||||
const assistantVisible = createMemo(() => assistantDerived().visible)
|
const assistantVisible = createMemo(() => assistantDerived().visible)
|
||||||
const assistantTailVisible = createMemo(() => assistantDerived().tail)
|
|
||||||
const reasoningHeading = createMemo(() => assistantDerived().reason)
|
const reasoningHeading = createMemo(() => assistantDerived().reason)
|
||||||
const showThinking = createMemo(() => {
|
const showThinking = createMemo(() => {
|
||||||
if (!working() || !!error()) return false
|
if (!working() || !!error()) return false
|
||||||
|
|
|
||||||
|
|
@ -366,21 +366,13 @@ export default function Share(props: {
|
||||||
<Suspense>
|
<Suspense>
|
||||||
<For each={filteredParts()}>
|
<For each={filteredParts()}>
|
||||||
{(part, partIndex) => {
|
{(part, partIndex) => {
|
||||||
const last = createMemo(
|
const last = () =>
|
||||||
() =>
|
data().messages.length === msgIndex() + 1 && filteredParts().length === partIndex() + 1
|
||||||
data().messages.length === msgIndex() + 1 &&
|
|
||||||
filteredParts().length === partIndex() + 1,
|
|
||||||
)
|
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
const hash = window.location.hash.slice(1)
|
const hash = window.location.hash.slice(1)
|
||||||
// Wait till all parts are loaded
|
// Wait till all parts are loaded
|
||||||
if (
|
if (hash !== "" && !hasScrolledToAnchor && last()) {
|
||||||
hash !== "" &&
|
|
||||||
!hasScrolledToAnchor &&
|
|
||||||
filteredParts().length === partIndex() + 1 &&
|
|
||||||
data().messages.length === msgIndex() + 1
|
|
||||||
) {
|
|
||||||
hasScrolledToAnchor = true
|
hasScrolledToAnchor = true
|
||||||
scrollToAnchor(hash)
|
scrollToAnchor(hash)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -83,12 +83,15 @@ export function createOverflow() {
|
||||||
return overflow()
|
return overflow()
|
||||||
},
|
},
|
||||||
ref(el: HTMLElement) {
|
ref(el: HTMLElement) {
|
||||||
|
const sync = () => {
|
||||||
|
setOverflow(el.scrollHeight > el.clientHeight + 1)
|
||||||
|
}
|
||||||
|
|
||||||
const ro = new ResizeObserver(() => {
|
const ro = new ResizeObserver(() => {
|
||||||
if (el.scrollHeight > el.clientHeight + 1) {
|
sync()
|
||||||
setOverflow(true)
|
|
||||||
}
|
|
||||||
return
|
|
||||||
})
|
})
|
||||||
|
|
||||||
|
sync()
|
||||||
ro.observe(el)
|
ro.observe(el)
|
||||||
|
|
||||||
onCleanup(() => {
|
onCleanup(() => {
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { parsePatch } from "diff"
|
import { parsePatch } from "diff"
|
||||||
import { createMemo } from "solid-js"
|
import { createMemo, For } from "solid-js"
|
||||||
import { ContentCode } from "./content-code"
|
import { ContentCode } from "./content-code"
|
||||||
import styles from "./content-diff.module.css"
|
import styles from "./content-diff.module.css"
|
||||||
|
|
||||||
|
|
@ -160,28 +160,37 @@ export function ContentDiff(props: Props) {
|
||||||
return (
|
return (
|
||||||
<div class={styles.root}>
|
<div class={styles.root}>
|
||||||
<div data-component="desktop">
|
<div data-component="desktop">
|
||||||
{rows().map((r) => (
|
<For each={rows()}>
|
||||||
<div data-component="diff-row" data-type={r.type}>
|
{(row) => (
|
||||||
<div data-slot="before" data-diff-type={r.type === "removed" || r.type === "modified" ? "removed" : ""}>
|
<div data-component="diff-row" data-type={row.type}>
|
||||||
<ContentCode code={r.left} flush lang={props.lang} />
|
<div
|
||||||
|
data-slot="before"
|
||||||
|
data-diff-type={row.type === "removed" || row.type === "modified" ? "removed" : ""}
|
||||||
|
>
|
||||||
|
<ContentCode code={row.left} flush lang={props.lang} />
|
||||||
|
</div>
|
||||||
|
<div data-slot="after" data-diff-type={row.type === "added" || row.type === "modified" ? "added" : ""}>
|
||||||
|
<ContentCode code={row.right} lang={props.lang} flush />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div data-slot="after" data-diff-type={r.type === "added" || r.type === "modified" ? "added" : ""}>
|
)}
|
||||||
<ContentCode code={r.right} lang={props.lang} flush />
|
</For>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div data-component="mobile">
|
<div data-component="mobile">
|
||||||
{mobileRows().map((block) => (
|
<For each={mobileRows()}>
|
||||||
<div data-component="diff-block" data-type={block.type}>
|
{(block) => (
|
||||||
{block.lines.map((line) => (
|
<div data-component="diff-block" data-type={block.type}>
|
||||||
<div data-diff-type={block.type === "removed" ? "removed" : block.type === "added" ? "added" : ""}>
|
<For each={block.lines}>
|
||||||
<ContentCode code={line} lang={props.lang} flush />
|
{(line) => (
|
||||||
</div>
|
<div data-diff-type={block.type === "removed" ? "removed" : block.type === "added" ? "added" : ""}>
|
||||||
))}
|
<ContentCode code={line} lang={props.lang} flush />
|
||||||
</div>
|
</div>
|
||||||
))}
|
)}
|
||||||
|
</For>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</For>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue