diff --git a/packages/app/src/components/prompt-input.tsx b/packages/app/src/components/prompt-input.tsx index 1ca085a42..b5a101994 100644 --- a/packages/app/src/components/prompt-input.tsx +++ b/packages/app/src/components/prompt-input.tsx @@ -403,15 +403,10 @@ export const PromptInput: Component = (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 = (props) => { onPaste={handlePaste} onCompositionStart={() => setComposing(true)} onCompositionEnd={() => setComposing(false)} + onBlur={handleBlur} onKeyDown={handleKeyDown} classList={{ "select-text": true, diff --git a/packages/app/src/components/session/session-header.tsx b/packages/app/src/components/session/session-header.tsx index 912e449cf..3003d0514 100644 --- a/packages/app/src/components/session/session-header.tsx +++ b/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() { {language.t("session.header.openIn")} { if (!OPEN_APPS.includes(value as OpenApp)) return setPrefs("app", value as OpenApp) diff --git a/packages/app/src/context/language.tsx b/packages/app/src/context/language.tsx index b21ec6d3c..905305d3a 100644 --- a/packages/app/src/context/language.tsx +++ b/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(() => - 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(() => normalizeLocale(store.locale)) const dict = createMemo(() => 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)) }, } }, diff --git a/packages/app/src/pages/layout.tsx b/packages/app/src/pages/layout.tsx index e280b2f92..29ba142e5 100644 --- a/packages/app/src/pages/layout.tsx +++ b/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) }} diff --git a/packages/app/src/pages/session.tsx b/packages/app/src/pages/session.tsx index 165845564..21ba4e7d7 100644 --- a/packages/app/src/pages/session.tsx +++ b/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) }) diff --git a/packages/app/src/pages/session/file-tabs.tsx b/packages/app/src/pages/session/file-tabs.tsx index 9e3a54311..ebc1f5922 100644 --- a/packages/app/src/pages/session/file-tabs.tsx +++ b/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) }} diff --git a/packages/app/src/pages/session/session-prompt-dock.tsx b/packages/app/src/pages/session/session-prompt-dock.tsx index 3f0b7a6e8..abe12bcb0 100644 --- a/packages/app/src/pages/session/session-prompt-dock.tsx +++ b/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() + 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: { <>
- -