فهرست منبع

perf(app): performance improvements

adamelmore 4 هفته پیش
والد
کامیت
dcc8d1a638

+ 23 - 0
packages/app/src/components/prompt-input.tsx

@@ -137,6 +137,8 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
   let scrollRef!: HTMLDivElement
   let slashPopoverRef!: HTMLDivElement
 
+  const mirror = { input: false }
+
   const scrollCursorIntoView = () => {
     const container = scrollRef
     const selection = window.getSelection()
@@ -651,6 +653,25 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
       () => prompt.current(),
       (currentParts) => {
         const inputParts = currentParts.filter((part) => part.type !== "image") as Prompt
+
+        if (mirror.input) {
+          mirror.input = false
+          if (isNormalizedEditor()) return
+
+          const selection = window.getSelection()
+          let cursorPosition: number | null = null
+          if (selection && selection.rangeCount > 0 && editorRef.contains(selection.anchorNode)) {
+            cursorPosition = getCursorPosition(editorRef)
+          }
+
+          renderEditor(inputParts)
+
+          if (cursorPosition !== null) {
+            setCursorPosition(editorRef, cursorPosition)
+          }
+          return
+        }
+
         const domParts = parseFromDOM()
         if (isNormalizedEditor() && isPromptEqual(inputParts, domParts)) return
 
@@ -765,6 +786,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
         setStore("savedPrompt", null)
       }
       if (prompt.dirty()) {
+        mirror.input = true
         prompt.set(DEFAULT_PROMPT, 0)
       }
       queueScroll()
@@ -795,6 +817,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
       setStore("savedPrompt", null)
     }
 
+    mirror.input = true
     prompt.set([...rawParts, ...images], cursorPosition)
     queueScroll()
   }

+ 9 - 5
packages/app/src/components/session-context-usage.tsx

@@ -26,13 +26,17 @@ export function SessionContextUsage(props: SessionContextUsageProps) {
   const view = createMemo(() => layout.view(sessionKey))
   const messages = createMemo(() => (params.id ? (sync.data.message[params.id] ?? []) : []))
 
+  const usd = createMemo(
+    () =>
+      new Intl.NumberFormat(language.locale(), {
+        style: "currency",
+        currency: "USD",
+      }),
+  )
+
   const cost = createMemo(() => {
-    const locale = language.locale()
     const total = messages().reduce((sum, x) => sum + (x.role === "assistant" ? x.cost : 0), 0)
-    return new Intl.NumberFormat(locale, {
-      style: "currency",
-      currency: "USD",
-    }).format(total)
+    return usd().format(total)
   })
 
   const context = createMemo(() => {

+ 9 - 5
packages/app/src/components/session/session-context-tab.tsx

@@ -26,6 +26,14 @@ export function SessionContextTab(props: SessionContextTabProps) {
   const sync = useSync()
   const language = useLanguage()
 
+  const usd = createMemo(
+    () =>
+      new Intl.NumberFormat(language.locale(), {
+        style: "currency",
+        currency: "USD",
+      }),
+  )
+
   const ctx = createMemo(() => {
     const last = findLast(props.messages(), (x) => {
       if (x.role !== "assistant") return false
@@ -62,12 +70,8 @@ export function SessionContextTab(props: SessionContextTabProps) {
   })
 
   const cost = createMemo(() => {
-    const locale = language.locale()
     const total = props.messages().reduce((sum, x) => sum + (x.role === "assistant" ? x.cost : 0), 0)
-    return new Intl.NumberFormat(locale, {
-      style: "currency",
-      currency: "USD",
-    }).format(total)
+    return usd().format(total)
   })
 
   const counts = createMemo(() => {

+ 49 - 8
packages/app/src/pages/session.tsx

@@ -1251,19 +1251,40 @@ export default function Page() {
     autoScroll.forceScrollToBottom()
   }
 
+  const closestMessage = (node: Element | null): HTMLElement | null => {
+    if (!node) return null
+    const match = node.closest?.("[data-message-id]") as HTMLElement | null
+    if (match) return match
+    const root = node.getRootNode?.()
+    if (root instanceof ShadowRoot) return closestMessage(root.host)
+    return null
+  }
+
   const getActiveMessageId = (container: HTMLDivElement) => {
+    const rect = container.getBoundingClientRect()
+    if (!rect.width || !rect.height) return
+
+    const x = Math.min(window.innerWidth - 1, Math.max(0, rect.left + rect.width / 2))
+    const y = Math.min(window.innerHeight - 1, Math.max(0, rect.top + 100))
+
+    const hit = document.elementFromPoint(x, y)
+    const host = closestMessage(hit)
+    const id = host?.dataset.messageId
+    if (id) return id
+
+    // Fallback: DOM query (handles edge hit-testing cases)
     const cutoff = container.scrollTop + 100
     const nodes = container.querySelectorAll<HTMLElement>("[data-message-id]")
-    let id: string | undefined
+    let last: string | undefined
 
     for (const node of nodes) {
       const next = node.dataset.messageId
       if (!next) continue
       if (node.offsetTop > cutoff) break
-      id = next
+      last = next
     }
 
-    return id
+    return last
   }
 
   const scheduleScrollSpy = (container: HTMLDivElement) => {
@@ -1900,6 +1921,8 @@ export default function Page() {
                     const [positions, setPositions] = createSignal<Record<string, number>>({})
                     const [draftTop, setDraftTop] = createSignal<number | undefined>(undefined)
 
+                    const empty = {} as Record<string, number>
+
                     const commentLabel = (range: SelectedLineRange) => {
                       const start = Math.min(range.start, range.end)
                       const end = Math.max(range.start, range.end)
@@ -1933,12 +1956,22 @@ export default function Page() {
                       return rect.top - wrapperRect.top + Math.max(0, (rect.height - 20) / 2)
                     }
 
+                    const equal = (a: Record<string, number>, b: Record<string, number>) => {
+                      const aKeys = Object.keys(a)
+                      const bKeys = Object.keys(b)
+                      if (aKeys.length !== bKeys.length) return false
+                      for (const key of aKeys) {
+                        if (a[key] !== b[key]) return false
+                      }
+                      return true
+                    }
+
                     const updateComments = () => {
                       const el = wrap
                       const root = getRoot()
                       if (!el || !root) {
-                        setPositions({})
-                        setDraftTop(undefined)
+                        setPositions((prev) => (Object.keys(prev).length === 0 ? prev : empty))
+                        setDraftTop((prev) => (prev === undefined ? prev : undefined))
                         return
                       }
 
@@ -1949,7 +1982,7 @@ export default function Page() {
                         next[comment.id] = markerTop(el, marker)
                       }
 
-                      setPositions(next)
+                      setPositions((prev) => (equal(prev, next) ? prev : next))
 
                       const range = commenting()
                       if (!range) {
@@ -1963,11 +1996,18 @@ export default function Page() {
                         return
                       }
 
-                      setDraftTop(markerTop(el, marker))
+                      const nextTop = markerTop(el, marker)
+                      setDraftTop((prev) => (prev === nextTop ? prev : nextTop))
                     }
 
+                    let commentFrame: number | undefined
+
                     const scheduleComments = () => {
-                      requestAnimationFrame(updateComments)
+                      if (commentFrame !== undefined) return
+                      commentFrame = requestAnimationFrame(() => {
+                        commentFrame = undefined
+                        updateComments()
+                      })
                     }
 
                     createEffect(() => {
@@ -2225,6 +2265,7 @@ export default function Page() {
                     )
 
                     onCleanup(() => {
+                      if (commentFrame !== undefined) cancelAnimationFrame(commentFrame)
                       for (const item of codeScroll) {
                         item.removeEventListener("scroll", handleCodeScroll)
                       }