Adam 6 месяцев назад
Родитель
Сommit
42aa28d512

+ 5 - 9
packages/app/src/components/prompt-input.tsx

@@ -403,15 +403,10 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
   const [composing, setComposing] = createSignal(false)
   const isImeComposing = (event: KeyboardEvent) => event.isComposing || composing() || event.keyCode === 229
 
-  createEffect(() => {
-    if (!isFocused()) closePopover()
-  })
-
-  // Safety: reset composing state on focus change to prevent stuck state
-  // This handles edge cases where compositionend event may not fire
-  createEffect(() => {
-    if (!isFocused()) setComposing(false)
-  })
+  const handleBlur = () => {
+    closePopover()
+    setComposing(false)
+  }
 
   const agentList = createMemo(() =>
     sync.data.agent
@@ -1118,6 +1113,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
               onPaste={handlePaste}
               onCompositionStart={() => setComposing(true)}
               onCompositionEnd={() => setComposing(false)}
+              onBlur={handleBlur}
               onKeyDown={handleKeyDown}
               classList={{
                 "select-text": true,

+ 1 - 16
packages/app/src/components/session/session-header.tsx

@@ -257,27 +257,12 @@ export function SessionHeader() {
     ] as const
   })
 
-  const checksReady = createMemo(() => {
-    if (platform.platform !== "desktop") return true
-    if (!platform.checkAppExists) return true
-    const list = apps()
-    return list.every((app) => exists[app.id] !== undefined)
-  })
-
   const [prefs, setPrefs] = persisted(Persist.global("open.app"), createStore({ app: "finder" as OpenApp }))
   const [menu, setMenu] = createStore({ open: false })
 
   const canOpen = createMemo(() => platform.platform === "desktop" && !!platform.openPath && server.isLocal())
   const current = createMemo(() => options().find((o) => o.id === prefs.app) ?? options()[0])
 
-  createEffect(() => {
-    if (platform.platform !== "desktop") return
-    if (!checksReady()) return
-    const value = prefs.app
-    if (options().some((o) => o.id === value)) return
-    setPrefs("app", options()[0]?.id ?? "finder")
-  })
-
   const openDir = (app: OpenApp) => {
     const directory = projectDirectory()
     if (!directory) return
@@ -398,7 +383,7 @@ export function SessionHeader() {
                               <DropdownMenu.Group>
                                 <DropdownMenu.GroupLabel>{language.t("session.header.openIn")}</DropdownMenu.GroupLabel>
                                 <DropdownMenu.RadioGroup
-                                  value={prefs.app}
+                                  value={current().id}
                                   onChange={(value) => {
                                     if (!OPEN_APPS.includes(value as OpenApp)) return
                                     setPrefs("app", value as OpenApp)

+ 6 - 10
packages/app/src/context/language.tsx

@@ -174,6 +174,10 @@ function detectLocale(): Locale {
   return "en"
 }
 
+function normalizeLocale(value: string): Locale {
+  return LOCALES.includes(value as Locale) ? (value as Locale) : "en"
+}
+
 export const { use: useLanguage, provider: LanguageProvider } = createSimpleContext({
   name: "Language",
   init: () => {
@@ -184,15 +188,7 @@ export const { use: useLanguage, provider: LanguageProvider } = createSimpleCont
       }),
     )
 
-    const locale = createMemo<Locale>(() =>
-      LOCALES.includes(store.locale as Locale) ? (store.locale as Locale) : "en",
-    )
-
-    createEffect(() => {
-      const current = locale()
-      if (store.locale === current) return
-      setStore("locale", current)
-    })
+    const locale = createMemo<Locale>(() => normalizeLocale(store.locale))
 
     const dict = createMemo<Dictionary>(() => DICT[locale()])
 
@@ -213,7 +209,7 @@ export const { use: useLanguage, provider: LanguageProvider } = createSimpleCont
       label,
       t,
       setLocale(next: Locale) {
-        setStore("locale", next)
+        setStore("locale", normalizeLocale(next))
       },
     }
   },

+ 10 - 11
packages/app/src/pages/layout.tsx

@@ -177,7 +177,12 @@ export default function Layout(props: ParentProps) {
 
   const sidebarHovering = createMemo(() => !layout.sidebar.opened() && state.hoverProject !== undefined)
   const sidebarExpanded = createMemo(() => layout.sidebar.opened() || sidebarHovering())
-  const clearHoverProjectSoon = () => queueMicrotask(() => setState("hoverProject", undefined))
+  const setHoverProject = (value: string | undefined) => {
+    setState("hoverProject", value)
+    if (value !== undefined) return
+    aim.reset()
+  }
+  const clearHoverProjectSoon = () => queueMicrotask(() => setHoverProject(undefined))
   const setHoverSession = (id: string | undefined) => setState("hoverSession", id)
 
   const hoverProjectData = createMemo(() => {
@@ -188,13 +193,7 @@ export default function Layout(props: ParentProps) {
 
   createEffect(() => {
     if (!layout.sidebar.opened()) return
-    aim.reset()
-    setState("hoverProject", undefined)
-  })
-
-  createEffect(() => {
-    if (state.hoverProject !== undefined) return
-    aim.reset()
+    setHoverProject(undefined)
   })
 
   const autoselecting = createMemo(() => {
@@ -225,7 +224,7 @@ export default function Layout(props: ParentProps) {
   const clearSidebarHoverState = () => {
     if (layout.sidebar.opened()) return
     setState("hoverSession", undefined)
-    setState("hoverProject", undefined)
+    setHoverProject(undefined)
   }
 
   const navigateWithSidebarReset = (href: string) => {
@@ -1490,7 +1489,7 @@ export default function Layout(props: ParentProps) {
   function handleDragStart(event: unknown) {
     const id = getDraggableId(event)
     if (!id) return
-    setState("hoverProject", undefined)
+    setHoverProject(undefined)
     setStore("activeProject", id)
   }
 
@@ -1924,7 +1923,7 @@ export default function Layout(props: ParentProps) {
             if (navLeave.current !== undefined) clearTimeout(navLeave.current)
             navLeave.current = window.setTimeout(() => {
               navLeave.current = undefined
-              setState("hoverProject", undefined)
+              setHoverProject(undefined)
               setState("hoverSession", undefined)
             }, 300)
           }}

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

@@ -1,4 +1,4 @@
-import { onCleanup, Show, Match, Switch, createMemo, createEffect, on } from "solid-js"
+import { onCleanup, Show, Match, Switch, createMemo, createEffect, on, onMount } from "solid-js"
 import { createMediaQuery } from "@solid-primitives/media"
 import { createResizeObserver } from "@solid-primitives/resize-observer"
 import { useLocal } from "@/context/local"
@@ -981,7 +981,7 @@ export default function Page() {
     consumePendingMessage: layout.pendingMessage.consume,
   })
 
-  createEffect(() => {
+  onMount(() => {
     document.addEventListener("keydown", handleKeyDown)
   })
 

+ 15 - 15
packages/app/src/pages/session/file-tabs.tsx

@@ -168,6 +168,13 @@ export function FileTabContent(props: { tab: string }) {
     draftTop: undefined as number | undefined,
   })
 
+  const setCommenting = (range: SelectedLineRange | null) => {
+    setNote("commenting", range)
+    scheduleComments()
+    if (!range) return
+    setNote("draft", "")
+  }
+
   const getRoot = () => {
     const el = wrap
     if (!el) return
@@ -260,13 +267,6 @@ export function FileTabContent(props: { tab: string }) {
     scheduleComments()
   })
 
-  createEffect(() => {
-    const range = note.commenting
-    scheduleComments()
-    if (!range) return
-    setNote("draft", "")
-  })
-
   createEffect(() => {
     const focus = comments.focus()
     const p = path()
@@ -278,7 +278,7 @@ export function FileTabContent(props: { tab: string }) {
     if (!target) return
 
     setNote("openedComment", target.id)
-    setNote("commenting", null)
+    setCommenting(null)
     file.setSelectedLines(p, target.selection)
     requestAnimationFrame(() => comments.clearFocus())
   })
@@ -438,16 +438,16 @@ export function FileTabContent(props: { tab: string }) {
           const p = path()
           if (!p) return
           file.setSelectedLines(p, range)
-          if (!range) setNote("commenting", null)
+          if (!range) setCommenting(null)
         }}
         onLineSelectionEnd={(range: SelectedLineRange | null) => {
           if (!range) {
-            setNote("commenting", null)
+            setCommenting(null)
             return
           }
 
           setNote("openedComment", null)
-          setNote("commenting", range)
+          setCommenting(range)
         }}
         overflow="scroll"
         class="select-text"
@@ -468,7 +468,7 @@ export function FileTabContent(props: { tab: string }) {
             onClick={() => {
               const p = path()
               if (!p) return
-              setNote("commenting", null)
+              setCommenting(null)
               setNote("openedComment", (current) => (current === comment.id ? null : comment.id))
               file.setSelectedLines(p, comment.selection)
             }}
@@ -483,12 +483,12 @@ export function FileTabContent(props: { tab: string }) {
               value={note.draft}
               selection={formatCommentLabel(range())}
               onInput={(value) => setNote("draft", value)}
-              onCancel={() => setNote("commenting", null)}
+              onCancel={() => setCommenting(null)}
               onSubmit={(value) => {
                 const p = path()
                 if (!p) return
                 addCommentToContext({ file: p, selection: range(), comment: value, origin: "file" })
-                setNote("commenting", null)
+                setCommenting(null)
               }}
               onPopoverFocusOut={(e: FocusEvent) => {
                 const current = e.currentTarget as HTMLDivElement
@@ -497,7 +497,7 @@ export function FileTabContent(props: { tab: string }) {
 
                 setTimeout(() => {
                   if (!document.activeElement || !current.contains(document.activeElement)) {
-                    setNote("commenting", null)
+                    setCommenting(null)
                   }
                 }, 0)
               }}

+ 24 - 15
packages/app/src/pages/session/session-prompt-dock.tsx

@@ -70,29 +70,28 @@ export function SessionPromptDock(props: {
     setSessionHandoff(sessionKey(), { prompt: previewPrompt() })
   })
 
-  const [responding, setResponding] = createSignal(false)
-
-  createEffect(
-    on(
-      () => permissionRequest()?.id,
-      () => setResponding(false),
-      { defer: true },
-    ),
-  )
+  const [responding, setResponding] = createSignal<string | undefined>()
+  const permissionResponding = () => {
+    const perm = permissionRequest()
+    if (!perm) return false
+    return responding() === perm.id
+  }
 
   const decide = (response: "once" | "always" | "reject") => {
     const perm = permissionRequest()
     if (!perm) return
-    if (responding()) return
+    if (responding() === perm.id) return
 
-    setResponding(true)
+    setResponding(perm.id)
     sdk.client.permission
       .respond({ sessionID: perm.sessionID, permissionID: perm.id, response })
       .catch((err: unknown) => {
         const message = err instanceof Error ? err.message : String(err)
         showToast({ title: language.t("common.requestFailed"), description: message })
       })
-      .finally(() => setResponding(false))
+      .finally(() => {
+        setResponding((id) => (id === perm.id ? undefined : id))
+      })
   }
 
   const done = createMemo(
@@ -218,18 +217,28 @@ export function SessionPromptDock(props: {
                     <>
                       <div />
                       <div data-slot="permission-footer-actions">
-                        <Button variant="ghost" size="normal" onClick={() => decide("reject")} disabled={responding()}>
+                        <Button
+                          variant="ghost"
+                          size="normal"
+                          onClick={() => decide("reject")}
+                          disabled={permissionResponding()}
+                        >
                           {language.t("ui.permission.deny")}
                         </Button>
                         <Button
                           variant="secondary"
                           size="normal"
                           onClick={() => decide("always")}
-                          disabled={responding()}
+                          disabled={permissionResponding()}
                         >
                           {language.t("ui.permission.allowAlways")}
                         </Button>
-                        <Button variant="primary" size="normal" onClick={() => decide("once")} disabled={responding()}>
+                        <Button
+                          variant="primary"
+                          size="normal"
+                          onClick={() => decide("once")}
+                          disabled={permissionResponding()}
+                        >
                           {language.t("ui.permission.allowOnce")}
                         </Button>
                       </div>