Forráskód Böngészése

refactor: simplify solid reactivity across app and web (#20497)

Shoubhit Dash 2 hete
szülő
commit
d540d363a7

+ 2 - 3
packages/app/src/pages/session.tsx

@@ -329,10 +329,9 @@ export default function Page() {
   const { params, sessionKey, tabs, view } = useSessionLayout()
 
   createEffect(() => {
-    if (!untrack(() => prompt.ready())) return
-    prompt.ready()
+    if (!prompt.ready()) return
     untrack(() => {
-      if (params.id || !prompt.ready()) return
+      if (params.id) return
       const text = searchParams.prompt
       if (!text) return
       prompt.set([{ type: "text", content: text, start: 0, end: text.length }], text.length)

+ 0 - 5
packages/ui/src/components/line-comment-annotations.tsx

@@ -294,11 +294,6 @@ export function createLineCommentState<T>(props: LineCommentStateProps<T>) {
     cancelDraft()
   }
 
-  createEffect(() => {
-    props.commenting()
-    setDraft("")
-  })
-
   return {
     draft,
     setDraft,

+ 6 - 13
packages/ui/src/components/line-comment.tsx

@@ -1,6 +1,6 @@
 import { useFilteredList } from "@opencode-ai/ui/hooks"
 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 { FileIcon } from "./file-icon"
 import { Icon } from "./icon"
@@ -210,7 +210,6 @@ export const LineCommentEditor = (props: LineCommentEditorProps) => {
   const refs = {
     textarea: undefined as HTMLTextAreaElement | undefined,
   }
-  const [text, setText] = createSignal(split.value)
   const [open, setOpen] = createSignal(false)
 
   function selectMention(item: { path: string } | undefined) {
@@ -220,10 +219,9 @@ export const LineCommentEditor = (props: LineCommentEditorProps) => {
     const query = currentMention()
     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
 
-    setText(value)
     split.onInput(value)
     closeMention()
 
@@ -257,10 +255,6 @@ export const LineCommentEditor = (props: LineCommentEditorProps) => {
       fn()
     }
 
-  createEffect(() => {
-    setText(split.value)
-  })
-
   const closeMention = () => {
     setOpen(false)
     mention.clear()
@@ -302,7 +296,7 @@ export const LineCommentEditor = (props: LineCommentEditorProps) => {
   }
 
   const submit = () => {
-    const value = text().trim()
+    const value = split.value.trim()
     if (!value) return
     split.onSubmit(value)
   }
@@ -322,10 +316,9 @@ export const LineCommentEditor = (props: LineCommentEditorProps) => {
           data-slot="line-comment-textarea"
           rows={split.rows ?? 3}
           placeholder={split.placeholder ?? i18n.t("ui.lineComment.placeholder")}
-          value={text()}
+          value={split.value}
           on:input={(e) => {
             const value = (e.currentTarget as HTMLTextAreaElement).value
-            setText(value)
             split.onInput(value)
             syncMention()
           }}
@@ -422,7 +415,7 @@ export const LineCommentEditor = (props: LineCommentEditorProps) => {
                   type="button"
                   data-slot="line-comment-action"
                   data-variant="primary"
-                  disabled={text().trim().length === 0}
+                  disabled={split.value.trim().length === 0}
                   on:mousedown={hold 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}>
               {split.cancelLabel ?? i18n.t("ui.common.cancel")}
             </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")}
             </Button>
           </Show>

+ 23 - 8
packages/ui/src/components/message-part.tsx

@@ -230,6 +230,19 @@ function createPacedValue(getValue: () => string, live?: () => boolean) {
   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) {
   if (!path) return ""
   if (!directory) return path
@@ -1373,8 +1386,7 @@ PART_MAPPING["text"] = function TextPartDisplay(props) {
   const streaming = createMemo(
     () => props.message.role === "assistant" && typeof (props.message as AssistantMessage).time.completed !== "number",
   )
-  const displayText = () => (part().text ?? "").trim()
-  const throttledText = createPacedValue(displayText, streaming)
+  const text = () => (part().text ?? "").trim()
   const isLastTextPart = createMemo(() => {
     const last = (data.store.part?.[props.message.id] ?? [])
       .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 handleCopy = async () => {
-    const content = displayText()
+    const content = text()
     if (!content) return
     await navigator.clipboard.writeText(content)
     setCopied(true)
@@ -1398,10 +1410,12 @@ PART_MAPPING["text"] = function TextPartDisplay(props) {
   }
 
   return (
-    <Show when={throttledText()}>
+    <Show when={text()}>
       <div data-component="text-part">
         <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>
         <Show when={showCopy()}>
           <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",
   )
   const text = () => part().text.trim()
-  const throttledText = createPacedValue(text, streaming)
 
   return (
-    <Show when={throttledText()}>
+    <Show when={text()}>
       <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>
     </Show>
   )

+ 1 - 4
packages/ui/src/components/session-turn.tsx

@@ -343,14 +343,12 @@ export function SessionTurn(
   })
   const assistantDerived = createMemo(() => {
     let visible = 0
-    let tail: "text" | "other" | undefined
     let reason: string | undefined
     const show = showReasoningSummaries()
     for (const message of assistantMessages()) {
       for (const part of list(data.store.part?.[message.id], emptyParts)) {
         if (partState(part, show) === "visible") {
           visible++
-          tail = part.type === "text" ? "text" : "other"
         }
         if (part.type === "reasoning" && 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 assistantTailVisible = createMemo(() => assistantDerived().tail)
   const reasoningHeading = createMemo(() => assistantDerived().reason)
   const showThinking = createMemo(() => {
     if (!working() || !!error()) return false

+ 3 - 11
packages/web/src/components/Share.tsx

@@ -366,21 +366,13 @@ export default function Share(props: {
                         <Suspense>
                           <For each={filteredParts()}>
                             {(part, partIndex) => {
-                              const last = createMemo(
-                                () =>
-                                  data().messages.length === msgIndex() + 1 &&
-                                  filteredParts().length === partIndex() + 1,
-                              )
+                              const last = () =>
+                                data().messages.length === msgIndex() + 1 && filteredParts().length === partIndex() + 1
 
                               onMount(() => {
                                 const hash = window.location.hash.slice(1)
                                 // Wait till all parts are loaded
-                                if (
-                                  hash !== "" &&
-                                  !hasScrolledToAnchor &&
-                                  filteredParts().length === partIndex() + 1 &&
-                                  data().messages.length === msgIndex() + 1
-                                ) {
+                                if (hash !== "" && !hasScrolledToAnchor && last()) {
                                   hasScrolledToAnchor = true
                                   scrollToAnchor(hash)
                                 }

+ 7 - 4
packages/web/src/components/share/common.tsx

@@ -83,12 +83,15 @@ export function createOverflow() {
       return overflow()
     },
     ref(el: HTMLElement) {
+      const sync = () => {
+        setOverflow(el.scrollHeight > el.clientHeight + 1)
+      }
+
       const ro = new ResizeObserver(() => {
-        if (el.scrollHeight > el.clientHeight + 1) {
-          setOverflow(true)
-        }
-        return
+        sync()
       })
+
+      sync()
       ro.observe(el)
 
       onCleanup(() => {

+ 28 - 19
packages/web/src/components/share/content-diff.tsx

@@ -1,5 +1,5 @@
 import { parsePatch } from "diff"
-import { createMemo } from "solid-js"
+import { createMemo, For } from "solid-js"
 import { ContentCode } from "./content-code"
 import styles from "./content-diff.module.css"
 
@@ -160,28 +160,37 @@ export function ContentDiff(props: Props) {
   return (
     <div class={styles.root}>
       <div data-component="desktop">
-        {rows().map((r) => (
-          <div data-component="diff-row" data-type={r.type}>
-            <div data-slot="before" data-diff-type={r.type === "removed" || r.type === "modified" ? "removed" : ""}>
-              <ContentCode code={r.left} flush lang={props.lang} />
-            </div>
-            <div data-slot="after" data-diff-type={r.type === "added" || r.type === "modified" ? "added" : ""}>
-              <ContentCode code={r.right} lang={props.lang} flush />
+        <For each={rows()}>
+          {(row) => (
+            <div data-component="diff-row" data-type={row.type}>
+              <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>
-        ))}
+          )}
+        </For>
       </div>
 
       <div data-component="mobile">
-        {mobileRows().map((block) => (
-          <div data-component="diff-block" data-type={block.type}>
-            {block.lines.map((line) => (
-              <div data-diff-type={block.type === "removed" ? "removed" : block.type === "added" ? "added" : ""}>
-                <ContentCode code={line} lang={props.lang} flush />
-              </div>
-            ))}
-          </div>
-        ))}
+        <For each={mobileRows()}>
+          {(block) => (
+            <div data-component="diff-block" data-type={block.type}>
+              <For each={block.lines}>
+                {(line) => (
+                  <div data-diff-type={block.type === "removed" ? "removed" : block.type === "added" ? "added" : ""}>
+                    <ContentCode code={line} lang={props.lang} flush />
+                  </div>
+                )}
+              </For>
+            </div>
+          )}
+        </For>
       </div>
     </div>
   )